mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-16 21:35:04 -04:00
Compare commits
23 Commits
v2.3.4.530
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46ce8e2701 | ||
|
|
c687bdb1fb | ||
|
|
b2d49164bc | ||
|
|
28bd80d3aa | ||
|
|
0ffcfccf1d | ||
|
|
3c4efa0226 | ||
|
|
50d31d0c5e | ||
|
|
f48c9f9f88 | ||
|
|
1ba2f26649 | ||
|
|
c880b6c09c | ||
|
|
6fca0d0b6c | ||
|
|
9907342055 | ||
|
|
71d1a59008 | ||
|
|
33fa39dc84 | ||
|
|
d133c82537 | ||
|
|
6b446e1404 | ||
|
|
b0e879da5c | ||
|
|
5edde8d9bd | ||
|
|
ef5d670c39 | ||
|
|
f568906876 | ||
|
|
331e92ac62 | ||
|
|
ec46b25be2 | ||
|
|
8b3837cb6e |
26
.github/workflows/close_invalid_issues.yml
vendored
Normal file
26
.github/workflows/close_invalid_issues.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: Close issues without labels
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
close-issue:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
sparse-checkout: |
|
||||||
|
.github
|
||||||
|
- name: Close issue if no labels found
|
||||||
|
if: join(github.event.issue.labels) == ''
|
||||||
|
run: |
|
||||||
|
gh issue comment ${{ github.event.issue.number }} --body ":wave: @${{ github.event.issue.user.login }}, this issue was closed automatically because it was created without following an issue template. Please update the issue following the correct template for this issue. Once updated please reply to this issue so we can review and re-open. In the future, use the [issue templates](https://github.com/${{ github.repository }}/issues/new/choose) instead of creating your own."
|
||||||
|
gh issue close ${{ github.event.issue.number }} --reason "not planned"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
@@ -9,7 +9,7 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '2.3.4'
|
majorVersion: '2.3.7'
|
||||||
minorVersion: $[counter('minorVersion', 1)]
|
minorVersion: $[counter('minorVersion', 1)]
|
||||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||||
@@ -17,7 +17,7 @@ variables:
|
|||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '8.0.405'
|
dotnetVersion: '8.0.405'
|
||||||
nodeVersion: '20.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.2'
|
innoVersion: '6.7.1'
|
||||||
windowsImage: 'windows-2025'
|
windowsImage: 'windows-2025'
|
||||||
linuxImage: 'ubuntu-24.04'
|
linuxImage: 'ubuntu-24.04'
|
||||||
macImage: 'macOS-15'
|
macImage: 'macOS-15'
|
||||||
|
|||||||
4
build.sh
4
build.sh
@@ -253,8 +253,10 @@ InstallInno()
|
|||||||
{
|
{
|
||||||
ProgressStart "Installing portable Inno Setup"
|
ProgressStart "Installing portable Inno Setup"
|
||||||
|
|
||||||
|
INNOVERSION=${INNOVERSION:-6.7.1}
|
||||||
|
|
||||||
rm -rf _inno
|
rm -rf _inno
|
||||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.2}.exe"
|
curl -s -L --output innosetup.exe "https://github.com/jrsoftware/issrc/releases/download/is-${INNOVERSION//./_}/innosetup-${INNOVERSION}.exe"
|
||||||
mkdir _inno
|
mkdir _inno
|
||||||
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
||||||
rm innosetup.exe
|
rm innosetup.exe
|
||||||
|
|||||||
@@ -133,6 +133,12 @@ module.exports = (env) => {
|
|||||||
{
|
{
|
||||||
source: 'frontend/src/Content/robots.txt',
|
source: 'frontend/src/Content/robots.txt',
|
||||||
destination: path.join(distFolder, 'Content/robots.txt')
|
destination: path.join(distFolder, 'Content/robots.txt')
|
||||||
|
},
|
||||||
|
|
||||||
|
// manifest.json and browserconfig.xml
|
||||||
|
{
|
||||||
|
source: 'frontend/src/Content/*.(json|xml)',
|
||||||
|
destination: path.join(distFolder, 'Content')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig>
|
|
||||||
<msapplication>
|
|
||||||
<tile>
|
|
||||||
<square150x150logo src="/Content/Images/Icons/mstile-150x150.png"/>
|
|
||||||
<TileColor>#00ccff</TileColor>
|
|
||||||
</tile>
|
|
||||||
</msapplication>
|
|
||||||
</browserconfig>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Prowlarr",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "android-chrome-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "android-chrome-512x512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": "../../../../",
|
|
||||||
"theme_color": "#3a3f51",
|
|
||||||
"background_color": "#3a3f51",
|
|
||||||
"display": "standalone"
|
|
||||||
}
|
|
||||||
11
frontend/src/Content/browserconfig.xml
Normal file
11
frontend/src/Content/browserconfig.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="__URL_BASE__/Content/Images/Icons/mstile-150x150.png" />
|
||||||
|
<TileColor>
|
||||||
|
#00ccff
|
||||||
|
</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
||||||
19
frontend/src/Content/manifest.json
Normal file
19
frontend/src/Content/manifest.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "__INSTANCE_NAME__",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": "__URL_BASE__/",
|
||||||
|
"theme_color": "#3a3f51",
|
||||||
|
"background_color": "#3a3f51",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
sizes="16x16"
|
sizes="16x16"
|
||||||
href="/Content/Images/Icons/favicon-16x16.png"
|
href="/Content/Images/Icons/favicon-16x16.png"
|
||||||
/>
|
/>
|
||||||
<link rel="manifest" href="/Content/Images/Icons/manifest.json" crossorigin="use-credentials" />
|
<link rel="manifest" href="/Content/manifest.json" crossorigin="use-credentials" />
|
||||||
<link
|
<link
|
||||||
rel="mask-icon"
|
rel="mask-icon"
|
||||||
href="/Content/Images/Icons/safari-pinned-tab.svg"
|
href="/Content/Images/Icons/safari-pinned-tab.svg"
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="msapplication-config"
|
name="msapplication-config"
|
||||||
content="/Content/Images/Icons/browserconfig.xml"
|
content="/Content/browserconfig.xml"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css">
|
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css">
|
||||||
|
|||||||
@@ -11,8 +11,11 @@
|
|||||||
<!-- Android/Apple Phone -->
|
<!-- Android/Apple Phone -->
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta
|
||||||
<meta name="format-detection" content="telephone=no">
|
name="apple-mobile-web-app-status-bar-style"
|
||||||
|
content="black-translucent"
|
||||||
|
/>
|
||||||
|
<meta name="format-detection" content="telephone=no" />
|
||||||
|
|
||||||
<meta name="description" content="Prowlarr" />
|
<meta name="description" content="Prowlarr" />
|
||||||
|
|
||||||
@@ -33,7 +36,11 @@
|
|||||||
sizes="16x16"
|
sizes="16x16"
|
||||||
href="/Content/Images/Icons/favicon-16x16.png"
|
href="/Content/Images/Icons/favicon-16x16.png"
|
||||||
/>
|
/>
|
||||||
<link rel="manifest" href="/Content/Images/Icons/manifest.json" crossorigin="use-credentials" />
|
<link
|
||||||
|
rel="manifest"
|
||||||
|
href="/Content/manifest.json"
|
||||||
|
crossorigin="use-credentials"
|
||||||
|
/>
|
||||||
<link
|
<link
|
||||||
rel="mask-icon"
|
rel="mask-icon"
|
||||||
href="/Content/Images/Icons/safari-pinned-tab.svg"
|
href="/Content/Images/Icons/safari-pinned-tab.svg"
|
||||||
@@ -45,10 +52,7 @@
|
|||||||
href="/favicon.ico"
|
href="/favicon.ico"
|
||||||
data-no-hash
|
data-no-hash
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta name="msapplication-config" content="/Content/browserconfig.xml" />
|
||||||
name="msapplication-config"
|
|
||||||
content="/Content/Images/Icons/browserconfig.xml"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="/Content/styles.css" />
|
<link rel="stylesheet" type="text/css" href="/Content/styles.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css" />
|
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css" />
|
||||||
@@ -59,7 +63,7 @@
|
|||||||
body {
|
body {
|
||||||
background-color: var(--pageBackground);
|
background-color: var(--pageBackground);
|
||||||
color: var(--textColor);
|
color: var(--textColor);
|
||||||
font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial,
|
font-family: 'Roboto', 'open sans', 'Helvetica Neue', Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,9 +213,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="sign-in">
|
<div class="sign-in">SIGN IN TO CONTINUE</div>
|
||||||
SIGN IN TO CONTINUE
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form
|
<form
|
||||||
role="form"
|
role="form"
|
||||||
@@ -230,8 +232,8 @@
|
|||||||
pattern=".{1,}"
|
pattern=".{1,}"
|
||||||
required
|
required
|
||||||
title="User name is required"
|
title="User name is required"
|
||||||
autoFocus="true"
|
autofocus="true"
|
||||||
autoCapitalize="false"
|
autocapitalize="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -282,16 +284,16 @@
|
|||||||
</body>
|
</body>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var yearSpan = document.getElementById("year");
|
var yearSpan = document.getElementById('year');
|
||||||
yearSpan.innerHTML = "2010-" + new Date().getFullYear();
|
yearSpan.innerHTML = '2010-' + new Date().getFullYear();
|
||||||
|
|
||||||
var copyDiv = document.getElementById("copy");
|
var copyDiv = document.getElementById('copy');
|
||||||
copyDiv.classList.remove("hidden");
|
copyDiv.classList.remove('hidden');
|
||||||
|
|
||||||
if (window.location.search.indexOf("loginFailed=true") > -1) {
|
if (window.location.search.indexOf('loginFailed=true') > -1) {
|
||||||
var loginFailedDiv = document.getElementById("login-failed");
|
var loginFailedDiv = document.getElementById('login-failed');
|
||||||
|
|
||||||
loginFailedDiv.classList.remove("hidden");
|
loginFailedDiv.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
var light = {
|
var light = {
|
||||||
@@ -311,7 +313,7 @@
|
|||||||
primaryHoverBorderColor: '#3483e7',
|
primaryHoverBorderColor: '#3483e7',
|
||||||
failedColor: '#f05050',
|
failedColor: '#f05050',
|
||||||
forgotPasswordColor: '#909fa7',
|
forgotPasswordColor: '#909fa7',
|
||||||
forgotPasswordAltColor: '#748690'
|
forgotPasswordAltColor: '#748690',
|
||||||
};
|
};
|
||||||
|
|
||||||
var dark = {
|
var dark = {
|
||||||
@@ -331,21 +333,16 @@
|
|||||||
primaryHoverBorderColor: '#3483e7',
|
primaryHoverBorderColor: '#3483e7',
|
||||||
failedColor: '#f05050',
|
failedColor: '#f05050',
|
||||||
forgotPasswordColor: '#737d83',
|
forgotPasswordColor: '#737d83',
|
||||||
forgotPasswordAltColor: '#546067'
|
forgotPasswordAltColor: '#546067',
|
||||||
};
|
};
|
||||||
|
|
||||||
var theme = "_THEME_";
|
var theme = '_THEME_';
|
||||||
var defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
var defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
var finalTheme = theme === 'dark' || (theme === 'auto' && defaultDark) ?
|
var finalTheme =
|
||||||
dark :
|
theme === 'dark' || (theme === 'auto' && defaultDark) ? dark : light;
|
||||||
light;
|
|
||||||
|
|
||||||
Object.entries(finalTheme).forEach(([key, value]) => {
|
Object.entries(finalTheme).forEach(([key, value]) => {
|
||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty(`--${key}`, value);
|
||||||
`--${key}`,
|
|
||||||
value
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,6 +5,5 @@
|
|||||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
<add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" />
|
<add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" />
|
||||||
<add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" />
|
<add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" />
|
||||||
<add key="FluentMigrator" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/FluentMigrator/nuget/v3/index.json" />
|
|
||||||
</packageSources>
|
</packageSources>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ namespace NzbDrone.Common.Composition
|
|||||||
static AssemblyLoader()
|
static AssemblyLoader()
|
||||||
{
|
{
|
||||||
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(ContainerResolveEventHandler);
|
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(ContainerResolveEventHandler);
|
||||||
RegisterSQLiteResolver();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IList<Assembly> Load(IList<string> assemblyNames)
|
public static IList<Assembly> Load(IList<string> assemblyNames)
|
||||||
@@ -23,6 +22,10 @@ namespace NzbDrone.Common.Composition
|
|||||||
toLoad.Add("Prowlarr.Common");
|
toLoad.Add("Prowlarr.Common");
|
||||||
toLoad.Add(OsInfo.IsWindows ? "Prowlarr.Windows" : "Prowlarr.Mono");
|
toLoad.Add(OsInfo.IsWindows ? "Prowlarr.Windows" : "Prowlarr.Mono");
|
||||||
|
|
||||||
|
var toRegisterResolver = new List<string> { "System.Data.SQLite" };
|
||||||
|
toRegisterResolver.AddRange(assemblyNames.Intersect(new[] { "Prowlarr.Core" }));
|
||||||
|
RegisterNativeResolver(toRegisterResolver);
|
||||||
|
|
||||||
var startupPath = AppDomain.CurrentDomain.BaseDirectory;
|
var startupPath = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
|
||||||
return toLoad
|
return toLoad
|
||||||
@@ -43,16 +46,18 @@ namespace NzbDrone.Common.Composition
|
|||||||
return AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
|
return AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RegisterSQLiteResolver()
|
public static void RegisterNativeResolver(IEnumerable<string> assemblyNames)
|
||||||
|
{
|
||||||
|
foreach (var name in assemblyNames)
|
||||||
{
|
{
|
||||||
// This ensures we look for sqlite3 using libsqlite3.so.0 on Linux and not libsqlite3.so which
|
// This ensures we look for sqlite3 using libsqlite3.so.0 on Linux and not libsqlite3.so which
|
||||||
// is less likely to exist.
|
// is less likely to exist.
|
||||||
var sqliteAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(
|
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(
|
||||||
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "System.Data.SQLite.dll"));
|
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"{name}.dll"));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
NativeLibrary.SetDllImportResolver(sqliteAssembly, LoadSqliteNativeLib);
|
NativeLibrary.SetDllImportResolver(assembly, LoadNativeLib);
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException)
|
catch (InvalidOperationException)
|
||||||
{
|
{
|
||||||
@@ -60,10 +65,27 @@ namespace NzbDrone.Common.Composition
|
|||||||
// Catch required for NzbDrone.Host tests
|
// Catch required for NzbDrone.Host tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static IntPtr LoadSqliteNativeLib(string libraryName, Assembly assembly, DllImportSearchPath? dllImportSearchPath)
|
private static IntPtr LoadNativeLib(string libraryName, Assembly assembly, DllImportSearchPath? dllImportSearchPath)
|
||||||
{
|
{
|
||||||
var mappedName = OsInfo.IsLinux && libraryName == "sqlite3" ? "libsqlite3.so.0" : libraryName;
|
ArgumentException.ThrowIfNullOrWhiteSpace(libraryName);
|
||||||
|
|
||||||
|
var mappedName = libraryName;
|
||||||
|
|
||||||
|
if (libraryName is "sqlite3" or "e_sqlite3")
|
||||||
|
{
|
||||||
|
if (OsInfo.IsLinux)
|
||||||
|
{
|
||||||
|
if (NativeLibrary.TryLoad(libraryName, assembly, dllImportSearchPath, out var libHandle))
|
||||||
|
{
|
||||||
|
return libHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
mappedName = "libsqlite3.so.0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return NativeLibrary.Load(mappedName, assembly, dllImportSearchPath);
|
return NativeLibrary.Load(mappedName, assembly, dllImportSearchPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ using NzbDrone.Common.Extensions;
|
|||||||
|
|
||||||
namespace NzbDrone.Common.Http
|
namespace NzbDrone.Common.Http
|
||||||
{
|
{
|
||||||
public class HttpUri : IEquatable<HttpUri>
|
public partial class HttpUri : IEquatable<HttpUri>
|
||||||
{
|
{
|
||||||
private static readonly Regex RegexUri = new Regex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+|\[[[A-F0-9:]+\])(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
private readonly string _uri;
|
private readonly string _uri;
|
||||||
public string FullUri => _uri;
|
public string FullUri => _uri;
|
||||||
|
|
||||||
|
[GeneratedRegex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+|\[[[A-F0-9:]+\])(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
|
||||||
|
private static partial Regex UriRegex();
|
||||||
|
|
||||||
public HttpUri(string uri)
|
public HttpUri(string uri)
|
||||||
{
|
{
|
||||||
_uri = uri ?? string.Empty;
|
_uri = uri ?? string.Empty;
|
||||||
@@ -70,9 +71,9 @@ namespace NzbDrone.Common.Http
|
|||||||
|
|
||||||
private void Parse()
|
private void Parse()
|
||||||
{
|
{
|
||||||
var parseSuccess = Uri.TryCreate(_uri, UriKind.RelativeOrAbsolute, out var uri);
|
var parseSuccess = Uri.TryCreate(_uri, UriKind.RelativeOrAbsolute, out _);
|
||||||
|
|
||||||
var match = RegexUri.Match(_uri);
|
var match = UriRegex().Match(_uri);
|
||||||
|
|
||||||
var scheme = match.Groups["scheme"];
|
var scheme = match.Groups["scheme"];
|
||||||
var host = match.Groups["host"];
|
var host = match.Groups["host"];
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using NzbDrone.Common.Instrumentation;
|
|||||||
namespace NzbDrone.Core.Datastore.Migration
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
[Maintenance(MigrationStage.BeforeAll, TransactionBehavior.None)]
|
[Maintenance(MigrationStage.BeforeAll, TransactionBehavior.None)]
|
||||||
public class DatabaseEngineVersionCheck : FluentMigrator.Migration
|
public class DatabaseEngineVersionCheck : ForwardOnlyMigration
|
||||||
{
|
{
|
||||||
protected readonly Logger _logger;
|
protected readonly Logger _logger;
|
||||||
|
|
||||||
@@ -22,11 +22,6 @@ namespace NzbDrone.Core.Datastore.Migration
|
|||||||
IfDatabase("postgres").Execute.WithConnection(LogPostgresVersion);
|
IfDatabase("postgres").Execute.WithConnection(LogPostgresVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Down()
|
|
||||||
{
|
|
||||||
// No-op
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LogSqliteVersion(IDbConnection conn, IDbTransaction tran)
|
private void LogSqliteVersion(IDbConnection conn, IDbTransaction tran)
|
||||||
{
|
{
|
||||||
using (var versionCmd = conn.CreateCommand())
|
using (var versionCmd = conn.CreateCommand())
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ using FluentMigrator.Runner.Generators;
|
|||||||
using FluentMigrator.Runner.Initialization;
|
using FluentMigrator.Runner.Initialization;
|
||||||
using FluentMigrator.Runner.Processors;
|
using FluentMigrator.Runner.Processors;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NLog;
|
using NLog;
|
||||||
using NLog.Extensions.Logging;
|
using NLog.Extensions.Logging;
|
||||||
|
|
||||||
@@ -20,13 +19,10 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
public class MigrationController : IMigrationController
|
public class MigrationController : IMigrationController
|
||||||
{
|
{
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
private readonly ILoggerProvider _migrationLoggerProvider;
|
|
||||||
|
|
||||||
public MigrationController(Logger logger,
|
public MigrationController(Logger logger)
|
||||||
ILoggerProvider migrationLoggerProvider)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_migrationLoggerProvider = migrationLoggerProvider;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
|
public void Migrate(string connectionString, MigrationContext migrationContext, DatabaseType databaseType)
|
||||||
@@ -35,16 +31,13 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
|
|
||||||
_logger.Info("*** Migrating {0} ***", connectionString);
|
_logger.Info("*** Migrating {0} ***", connectionString);
|
||||||
|
|
||||||
ServiceProvider serviceProvider;
|
|
||||||
|
|
||||||
var db = databaseType == DatabaseType.SQLite ? "sqlite" : "postgres";
|
var db = databaseType == DatabaseType.SQLite ? "sqlite" : "postgres";
|
||||||
|
|
||||||
serviceProvider = new ServiceCollection()
|
var serviceProvider = new ServiceCollection()
|
||||||
.AddLogging(b => b.AddNLog())
|
.AddLogging(b => b.AddNLog())
|
||||||
.AddFluentMigratorCore()
|
.AddFluentMigratorCore()
|
||||||
.Configure<RunnerOptions>(cfg => cfg.IncludeUntaggedMaintenances = true)
|
.Configure<RunnerOptions>(cfg => cfg.IncludeUntaggedMaintenances = true)
|
||||||
.ConfigureRunner(
|
.ConfigureRunner(builder => builder
|
||||||
builder => builder
|
|
||||||
.AddPostgres()
|
.AddPostgres()
|
||||||
.AddNzbDroneSQLite()
|
.AddNzbDroneSQLite()
|
||||||
.WithGlobalConnectionString(connectionString)
|
.WithGlobalConnectionString(connectionString)
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
using FluentMigrator;
|
using System.Data;
|
||||||
|
using FluentMigrator;
|
||||||
using FluentMigrator.Builders.Create;
|
using FluentMigrator.Builders.Create;
|
||||||
using FluentMigrator.Builders.Create.Table;
|
using FluentMigrator.Builders.Create.Table;
|
||||||
using FluentMigrator.Runner;
|
using FluentMigrator.Runner;
|
||||||
using FluentMigrator.Runner.BatchParser;
|
using FluentMigrator.Runner.BatchParser;
|
||||||
|
using FluentMigrator.Runner.Generators;
|
||||||
using FluentMigrator.Runner.Generators.SQLite;
|
using FluentMigrator.Runner.Generators.SQLite;
|
||||||
|
using FluentMigrator.Runner.Initialization;
|
||||||
|
using FluentMigrator.Runner.Processors;
|
||||||
using FluentMigrator.Runner.Processors.SQLite;
|
using FluentMigrator.Runner.Processors.SQLite;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||||
{
|
{
|
||||||
@@ -16,23 +22,49 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
return expressionRoot.Table(name).WithColumn("Id").AsInt32().PrimaryKey().Identity();
|
return expressionRoot.Table(name).WithColumn("Id").AsInt32().PrimaryKey().Identity();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddParameter(this System.Data.IDbCommand command, object value)
|
public static IDbCommand CreateCommand(this IDbConnection conn, IDbTransaction tran, string query)
|
||||||
|
{
|
||||||
|
var command = conn.CreateCommand();
|
||||||
|
command.Transaction = tran;
|
||||||
|
command.CommandText = query;
|
||||||
|
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddParameter(this IDbCommand command, object value)
|
||||||
{
|
{
|
||||||
var parameter = command.CreateParameter();
|
var parameter = command.CreateParameter();
|
||||||
parameter.Value = value;
|
parameter.Value = value;
|
||||||
command.Parameters.Add(parameter);
|
command.Parameters.Add(parameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IMigrationRunnerBuilder AddNzbDroneSQLite(this IMigrationRunnerBuilder builder)
|
public static IMigrationRunnerBuilder AddNzbDroneSQLite(this IMigrationRunnerBuilder builder, bool binaryGuid = false, bool useStrictTables = false)
|
||||||
{
|
{
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddTransient<SQLiteBatchParser>()
|
.AddTransient<SQLiteBatchParser>()
|
||||||
.AddScoped<SQLiteDbFactory>()
|
.AddScoped<SQLiteDbFactory>()
|
||||||
.AddScoped<NzbDroneSQLiteProcessor>()
|
.AddScoped<NzbDroneSQLiteProcessor>(sp =>
|
||||||
|
{
|
||||||
|
var factory = sp.GetService<SQLiteDbFactory>();
|
||||||
|
var logger = sp.GetService<ILogger<NzbDroneSQLiteProcessor>>();
|
||||||
|
var options = sp.GetService<IOptionsSnapshot<ProcessorOptions>>();
|
||||||
|
var connectionStringAccessor = sp.GetService<IConnectionStringAccessor>();
|
||||||
|
var sqliteQuoter = new SQLiteQuoter(false);
|
||||||
|
return new NzbDroneSQLiteProcessor(factory, sp.GetService<SQLiteGenerator>(), logger, options, connectionStringAccessor, sp, sqliteQuoter);
|
||||||
|
})
|
||||||
|
.AddScoped<ISQLiteTypeMap>(_ => new NzbDroneSQLiteTypeMap(useStrictTables))
|
||||||
.AddScoped<IMigrationProcessor>(sp => sp.GetRequiredService<NzbDroneSQLiteProcessor>())
|
.AddScoped<IMigrationProcessor>(sp => sp.GetRequiredService<NzbDroneSQLiteProcessor>())
|
||||||
.AddScoped<SQLiteQuoter>()
|
.AddScoped(
|
||||||
.AddScoped<SQLiteGenerator>()
|
sp =>
|
||||||
|
{
|
||||||
|
var typeMap = sp.GetRequiredService<ISQLiteTypeMap>();
|
||||||
|
return new SQLiteGenerator(
|
||||||
|
new SQLiteQuoter(binaryGuid),
|
||||||
|
typeMap,
|
||||||
|
new OptionsWrapper<GeneratorOptions>(new GeneratorOptions()));
|
||||||
|
})
|
||||||
.AddScoped<IMigrationGenerator>(sp => sp.GetRequiredService<SQLiteGenerator>());
|
.AddScoped<IMigrationGenerator>(sp => sp.GetRequiredService<SQLiteGenerator>());
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
{
|
{
|
||||||
public class NzbDroneSQLiteProcessor : SQLiteProcessor
|
public class NzbDroneSQLiteProcessor : SQLiteProcessor
|
||||||
{
|
{
|
||||||
|
private readonly SQLiteQuoter _quoter;
|
||||||
|
|
||||||
public NzbDroneSQLiteProcessor(SQLiteDbFactory factory,
|
public NzbDroneSQLiteProcessor(SQLiteDbFactory factory,
|
||||||
SQLiteGenerator generator,
|
SQLiteGenerator generator,
|
||||||
ILogger<NzbDroneSQLiteProcessor> logger,
|
ILogger<NzbDroneSQLiteProcessor> logger,
|
||||||
@@ -24,6 +26,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
SQLiteQuoter quoter)
|
SQLiteQuoter quoter)
|
||||||
: base(factory, generator, logger, options, connectionStringAccessor, serviceProvider, quoter)
|
: base(factory, generator, logger, options, connectionStringAccessor, serviceProvider, quoter)
|
||||||
{
|
{
|
||||||
|
_quoter = quoter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Process(AlterColumnExpression expression)
|
public override void Process(AlterColumnExpression expression)
|
||||||
@@ -35,7 +38,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
|
|
||||||
if (columnIndex == -1)
|
if (columnIndex == -1)
|
||||||
{
|
{
|
||||||
throw new ApplicationException(string.Format("Column {0} does not exist on table {1}.", expression.Column.Name, expression.TableName));
|
throw new ApplicationException($"Column {expression.Column.Name} does not exist on table {expression.TableName}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
columnDefinitions[columnIndex] = expression.Column;
|
columnDefinitions[columnIndex] = expression.Column;
|
||||||
@@ -45,6 +48,28 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
ProcessAlterTable(tableDefinition);
|
ProcessAlterTable(tableDefinition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void Process(AlterDefaultConstraintExpression expression)
|
||||||
|
{
|
||||||
|
var tableDefinition = GetTableSchema(expression.TableName);
|
||||||
|
|
||||||
|
var columnDefinitions = tableDefinition.Columns.ToList();
|
||||||
|
var columnIndex = columnDefinitions.FindIndex(c => c.Name == expression.ColumnName);
|
||||||
|
|
||||||
|
if (columnIndex == -1)
|
||||||
|
{
|
||||||
|
throw new ApplicationException($"Column {expression.ColumnName} does not exist on table {expression.TableName}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var changedColumn = columnDefinitions[columnIndex];
|
||||||
|
changedColumn.DefaultValue = expression.DefaultValue;
|
||||||
|
|
||||||
|
columnDefinitions[columnIndex] = changedColumn;
|
||||||
|
|
||||||
|
tableDefinition.Columns = columnDefinitions;
|
||||||
|
|
||||||
|
ProcessAlterTable(tableDefinition);
|
||||||
|
}
|
||||||
|
|
||||||
public override void Process(DeleteColumnExpression expression)
|
public override void Process(DeleteColumnExpression expression)
|
||||||
{
|
{
|
||||||
var tableDefinition = GetTableSchema(expression.TableName);
|
var tableDefinition = GetTableSchema(expression.TableName);
|
||||||
@@ -62,7 +87,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
|
|
||||||
if (columnsToRemove.Any())
|
if (columnsToRemove.Any())
|
||||||
{
|
{
|
||||||
throw new ApplicationException(string.Format("Column {0} does not exist on table {1}.", columnsToRemove.First(), expression.TableName));
|
throw new ApplicationException($"Column {columnsToRemove.First()} does not exist on table {expression.TableName}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
ProcessAlterTable(tableDefinition);
|
ProcessAlterTable(tableDefinition);
|
||||||
@@ -78,12 +103,12 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
|
|
||||||
if (columnIndex == -1)
|
if (columnIndex == -1)
|
||||||
{
|
{
|
||||||
throw new ApplicationException(string.Format("Column {0} does not exist on table {1}.", expression.OldName, expression.TableName));
|
throw new ApplicationException($"Column {expression.OldName} does not exist on table {expression.TableName}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (columnDefinitions.Any(c => c.Name == expression.NewName))
|
if (columnDefinitions.Any(c => c.Name == expression.NewName))
|
||||||
{
|
{
|
||||||
throw new ApplicationException(string.Format("Column {0} already exists on table {1}.", expression.NewName, expression.TableName));
|
throw new ApplicationException($"Column {expression.NewName} already exists on table {expression.TableName}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
oldColumnDefinitions[columnIndex] = (ColumnDefinition)columnDefinitions[columnIndex].Clone();
|
oldColumnDefinitions[columnIndex] = (ColumnDefinition)columnDefinitions[columnIndex].Clone();
|
||||||
@@ -128,21 +153,20 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
}
|
}
|
||||||
|
|
||||||
// What is the cleanest way to do this? Add function to Generator?
|
// What is the cleanest way to do this? Add function to Generator?
|
||||||
var quoter = new SQLiteQuoter();
|
var columnsToInsert = string.Join(", ", tableDefinition.Columns.Select(c => _quoter.QuoteColumnName(c.Name)));
|
||||||
var columnsToInsert = string.Join(", ", tableDefinition.Columns.Select(c => quoter.QuoteColumnName(c.Name)));
|
var columnsToFetch = string.Join(", ", (oldColumnDefinitions ?? tableDefinition.Columns).Select(c => _quoter.QuoteColumnName(c.Name)));
|
||||||
var columnsToFetch = string.Join(", ", (oldColumnDefinitions ?? tableDefinition.Columns).Select(c => quoter.QuoteColumnName(c.Name)));
|
|
||||||
|
|
||||||
Process(new CreateTableExpression() { TableName = tempTableName, Columns = tableDefinition.Columns.ToList() });
|
Process(new CreateTableExpression { TableName = tempTableName, Columns = tableDefinition.Columns.ToList() });
|
||||||
|
|
||||||
Process(string.Format("INSERT INTO {0} ({1}) SELECT {2} FROM {3}", quoter.QuoteTableName(tempTableName), columnsToInsert, columnsToFetch, quoter.QuoteTableName(tableName)));
|
Process($"INSERT INTO {_quoter.QuoteTableName(tempTableName)} ({columnsToInsert}) SELECT {columnsToFetch} FROM {_quoter.QuoteTableName(tableName)}");
|
||||||
|
|
||||||
Process(new DeleteTableExpression() { TableName = tableName });
|
Process(new DeleteTableExpression { TableName = tableName });
|
||||||
|
|
||||||
Process(new RenameTableExpression() { OldName = tempTableName, NewName = tableName });
|
Process(new RenameTableExpression { OldName = tempTableName, NewName = tableName });
|
||||||
|
|
||||||
foreach (var index in tableDefinition.Indexes)
|
foreach (var index in tableDefinition.Indexes)
|
||||||
{
|
{
|
||||||
Process(new CreateIndexExpression() { Index = index });
|
Process(new CreateIndexExpression { Index = index });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using System.Data;
|
||||||
|
using FluentMigrator.Runner.Generators.Base;
|
||||||
|
using FluentMigrator.Runner.Generators.SQLite;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
// Based on https://github.com/fluentmigrator/fluentmigrator/blob/v6.2.0/src/FluentMigrator.Runner.SQLite/Generators/SQLite/SQLiteTypeMap.cs
|
||||||
|
public sealed class NzbDroneSQLiteTypeMap : TypeMapBase, ISQLiteTypeMap
|
||||||
|
{
|
||||||
|
public bool UseStrictTables { get; }
|
||||||
|
|
||||||
|
public NzbDroneSQLiteTypeMap(bool useStrictTables = false)
|
||||||
|
{
|
||||||
|
UseStrictTables = useStrictTables;
|
||||||
|
|
||||||
|
SetupTypeMaps();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must be kept in sync with upstream
|
||||||
|
protected override void SetupTypeMaps()
|
||||||
|
{
|
||||||
|
SetTypeMap(DbType.Binary, "BLOB");
|
||||||
|
SetTypeMap(DbType.Byte, "INTEGER");
|
||||||
|
SetTypeMap(DbType.Int16, "INTEGER");
|
||||||
|
SetTypeMap(DbType.Int32, "INTEGER");
|
||||||
|
SetTypeMap(DbType.Int64, "INTEGER");
|
||||||
|
SetTypeMap(DbType.SByte, "INTEGER");
|
||||||
|
SetTypeMap(DbType.UInt16, "INTEGER");
|
||||||
|
SetTypeMap(DbType.UInt32, "INTEGER");
|
||||||
|
SetTypeMap(DbType.UInt64, "INTEGER");
|
||||||
|
|
||||||
|
if (!UseStrictTables)
|
||||||
|
{
|
||||||
|
SetTypeMap(DbType.Currency, "NUMERIC");
|
||||||
|
SetTypeMap(DbType.Decimal, "NUMERIC");
|
||||||
|
SetTypeMap(DbType.Double, "NUMERIC");
|
||||||
|
SetTypeMap(DbType.Single, "NUMERIC");
|
||||||
|
SetTypeMap(DbType.VarNumeric, "NUMERIC");
|
||||||
|
SetTypeMap(DbType.Date, "DATETIME");
|
||||||
|
SetTypeMap(DbType.DateTime, "DATETIME");
|
||||||
|
SetTypeMap(DbType.DateTime2, "DATETIME");
|
||||||
|
SetTypeMap(DbType.Time, "DATETIME");
|
||||||
|
SetTypeMap(DbType.Guid, "UNIQUEIDENTIFIER");
|
||||||
|
|
||||||
|
// Custom so that we can use DateTimeOffset in Postgres for appropriate DB typing
|
||||||
|
SetTypeMap(DbType.DateTimeOffset, "DATETIME");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetTypeMap(DbType.Currency, "TEXT");
|
||||||
|
SetTypeMap(DbType.Decimal, "TEXT");
|
||||||
|
SetTypeMap(DbType.Double, "REAL");
|
||||||
|
SetTypeMap(DbType.Single, "REAL");
|
||||||
|
SetTypeMap(DbType.VarNumeric, "TEXT");
|
||||||
|
SetTypeMap(DbType.Date, "TEXT");
|
||||||
|
SetTypeMap(DbType.DateTime, "TEXT");
|
||||||
|
SetTypeMap(DbType.DateTime2, "TEXT");
|
||||||
|
SetTypeMap(DbType.Time, "TEXT");
|
||||||
|
SetTypeMap(DbType.Guid, "TEXT");
|
||||||
|
|
||||||
|
// Custom so that we can use DateTimeOffset in Postgres for appropriate DB typing
|
||||||
|
SetTypeMap(DbType.DateTimeOffset, "TEXT");
|
||||||
|
}
|
||||||
|
|
||||||
|
SetTypeMap(DbType.AnsiString, "TEXT");
|
||||||
|
SetTypeMap(DbType.String, "TEXT");
|
||||||
|
SetTypeMap(DbType.AnsiStringFixedLength, "TEXT");
|
||||||
|
SetTypeMap(DbType.StringFixedLength, "TEXT");
|
||||||
|
SetTypeMap(DbType.Boolean, "INTEGER");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string GetTypeMap(DbType type, int? size, int? precision)
|
||||||
|
{
|
||||||
|
return base.GetTypeMap(type, size: null, precision: null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -120,7 +120,7 @@ namespace NzbDrone.Core.Indexers.Headphones
|
|||||||
baseUrl += "&apikey=" + Settings.ApiKey;
|
baseUrl += "&apikey=" + Settings.ApiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchCriteria.Limit.HasValue)
|
if (searchCriteria.Limit is > 0)
|
||||||
{
|
{
|
||||||
parameters.Add("limit", searchCriteria.Limit.ToString());
|
parameters.Add("limit", searchCriteria.Limit.ToString());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
|||||||
searchUrl += "&apikey=" + Settings.ApiKey;
|
searchUrl += "&apikey=" + Settings.ApiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchCriteria.Limit.HasValue)
|
if (searchCriteria.Limit is > 0)
|
||||||
{
|
{
|
||||||
parameters.Set("limit", searchCriteria.Limit.ToString());
|
parameters.Set("limit", searchCriteria.Limit.ToString());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ using NzbDrone.Core.Parser.Model;
|
|||||||
|
|
||||||
namespace NzbDrone.Core.Indexers.Definitions
|
namespace NzbDrone.Core.Indexers.Definitions
|
||||||
{
|
{
|
||||||
|
[Obsolete("Migrated to YAML for Torznab API")]
|
||||||
public class SceneTime : TorrentIndexerBase<SceneTimeSettings>
|
public class SceneTime : TorrentIndexerBase<SceneTimeSettings>
|
||||||
{
|
{
|
||||||
public override string Name => "SceneTime";
|
public override string Name => "SceneTime";
|
||||||
@@ -41,7 +42,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
|||||||
|
|
||||||
public override IParseIndexerResponse GetParser()
|
public override IParseIndexerResponse GetParser()
|
||||||
{
|
{
|
||||||
return new SceneTimeParser(Settings, Capabilities.Categories);
|
return new SceneTimeParser(Settings, Capabilities.Categories, _logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||||
@@ -59,7 +60,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
|||||||
return CookieUtil.CookieHeaderToDictionary(Settings.Cookie);
|
return CookieUtil.CookieHeaderToDictionary(Settings.Cookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IndexerCapabilities SetCapabilities()
|
private static IndexerCapabilities SetCapabilities()
|
||||||
{
|
{
|
||||||
var caps = new IndexerCapabilities
|
var caps = new IndexerCapabilities
|
||||||
{
|
{
|
||||||
@@ -213,11 +214,13 @@ namespace NzbDrone.Core.Indexers.Definitions
|
|||||||
{
|
{
|
||||||
private readonly SceneTimeSettings _settings;
|
private readonly SceneTimeSettings _settings;
|
||||||
private readonly IndexerCapabilitiesCategories _categories;
|
private readonly IndexerCapabilitiesCategories _categories;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public SceneTimeParser(SceneTimeSettings settings, IndexerCapabilitiesCategories categories)
|
public SceneTimeParser(SceneTimeSettings settings, IndexerCapabilitiesCategories categories, Logger logger)
|
||||||
{
|
{
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
_categories = categories;
|
_categories = categories;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||||
@@ -227,20 +230,22 @@ namespace NzbDrone.Core.Indexers.Definitions
|
|||||||
var parser = new HtmlParser();
|
var parser = new HtmlParser();
|
||||||
using var dom = parser.ParseDocument(indexerResponse.Content);
|
using var dom = parser.ParseDocument(indexerResponse.Content);
|
||||||
|
|
||||||
var table = dom.QuerySelector("table.movehere");
|
var table = dom.QuerySelector("table#torrenttable");
|
||||||
if (table == null)
|
if (table == null)
|
||||||
{
|
{
|
||||||
return releaseInfos; // no results
|
_logger.Error("No results, table element is not present in page.");
|
||||||
|
return releaseInfos;
|
||||||
}
|
}
|
||||||
|
|
||||||
var headerColumns = table.QuerySelectorAll("thead > tr > th.cat_Head")
|
var headerColumns = table.QuerySelectorAll("thead > tr > th")
|
||||||
.Select(x => x.GetAttribute("title").IsNotNullOrWhiteSpace() ? x.GetAttribute("title") : x.TextContent)
|
.Select(x => x.GetAttribute("title") ?? x.QuerySelector("a[title]")?.GetAttribute("title") ?? x.TextContent)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var categoryIndex = headerColumns.FindIndex(x => x.Equals("Type", StringComparison.OrdinalIgnoreCase));
|
var categoryIndex = headerColumns.FindIndex(x => x.Equals("Type", StringComparison.OrdinalIgnoreCase));
|
||||||
var nameIndex = headerColumns.FindIndex(x => x.Equals("Name", StringComparison.OrdinalIgnoreCase));
|
var nameIndex = headerColumns.FindIndex(x => x.Equals("Name", StringComparison.OrdinalIgnoreCase));
|
||||||
var sizeIndex = headerColumns.FindIndex(x => x.Equals("Size", StringComparison.OrdinalIgnoreCase));
|
var sizeIndex = headerColumns.FindIndex(x => x.Equals("Size", StringComparison.OrdinalIgnoreCase));
|
||||||
var seedersIndex = headerColumns.FindIndex(x => x.Equals("Seeder(s)", StringComparison.OrdinalIgnoreCase));
|
var seedersIndex = headerColumns.FindIndex(x => x.Equals("Seeders", StringComparison.OrdinalIgnoreCase));
|
||||||
var leechersIndex = headerColumns.FindIndex(x => x.Equals("Leecher(s)", StringComparison.OrdinalIgnoreCase));
|
var leechersIndex = headerColumns.FindIndex(x => x.Equals("Leechers", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
var rows = table.QuerySelectorAll("tbody > tr");
|
var rows = table.QuerySelectorAll("tbody > tr");
|
||||||
|
|
||||||
@@ -248,7 +253,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
|||||||
{
|
{
|
||||||
var qDescCol = row.Children[nameIndex];
|
var qDescCol = row.Children[nameIndex];
|
||||||
var qLink = qDescCol.QuerySelector("a");
|
var qLink = qDescCol.QuerySelector("a");
|
||||||
var title = qLink.QuerySelector("span.torrent-text").TextContent.Trim();
|
var title = qLink.QuerySelector("span.bw-torrent-name").TextContent.Trim();
|
||||||
|
|
||||||
var infoUrl = _settings.BaseUrl + qLink.GetAttribute("href")?.TrimStart('/');
|
var infoUrl = _settings.BaseUrl + qLink.GetAttribute("href")?.TrimStart('/');
|
||||||
var torrentId = ParseUtil.GetArgumentFromQueryString(infoUrl, "id");
|
var torrentId = ParseUtil.GetArgumentFromQueryString(infoUrl, "id");
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ using NzbDrone.Core.Parser.Model;
|
|||||||
|
|
||||||
namespace NzbDrone.Core.Indexers.Definitions
|
namespace NzbDrone.Core.Indexers.Definitions
|
||||||
{
|
{
|
||||||
|
[Obsolete("Site does not allow automation")]
|
||||||
public class ZonaQ : TorrentIndexerBase<UserPassTorrentBaseSettings>
|
public class ZonaQ : TorrentIndexerBase<UserPassTorrentBaseSettings>
|
||||||
{
|
{
|
||||||
public override string Name => "ZonaQ";
|
public override string Name => "ZonaQ";
|
||||||
|
|||||||
@@ -168,7 +168,7 @@
|
|||||||
"GeneralSettings": "通用设置",
|
"GeneralSettings": "通用设置",
|
||||||
"GeneralSettingsSummary": "端口、SSL、用户名/密码、代理、分析、更新",
|
"GeneralSettingsSummary": "端口、SSL、用户名/密码、代理、分析、更新",
|
||||||
"Genre": "类型",
|
"Genre": "类型",
|
||||||
"GrabReleases": "抓取版本",
|
"GrabReleases": "抓取资源",
|
||||||
"GrabTitle": "抓取标题",
|
"GrabTitle": "抓取标题",
|
||||||
"Grabbed": "已抓取",
|
"Grabbed": "已抓取",
|
||||||
"Grabs": "抓取",
|
"Grabs": "抓取",
|
||||||
@@ -621,7 +621,7 @@
|
|||||||
"DownloadClientFreeboxSettingsAppToken": "App Token",
|
"DownloadClientFreeboxSettingsAppToken": "App Token",
|
||||||
"DownloadClientFreeboxSettingsAppTokenHelpText": "创建访问 Freebox API 所需的 App token(即 “ app_token”)",
|
"DownloadClientFreeboxSettingsAppTokenHelpText": "创建访问 Freebox API 所需的 App token(即 “ app_token”)",
|
||||||
"DownloadClientQbittorrentSettingsInitialStateHelpText": "添加到 qBittorrent 的种子的初始状态。 请注意,强制做种不遵守种子限制",
|
"DownloadClientQbittorrentSettingsInitialStateHelpText": "添加到 qBittorrent 的种子的初始状态。 请注意,强制做种不遵守种子限制",
|
||||||
"GrabRelease": "抓取版本",
|
"GrabRelease": "抓取资源",
|
||||||
"ManualGrab": "手动抓取",
|
"ManualGrab": "手动抓取",
|
||||||
"OverrideAndAddToDownloadClient": "覆盖并添加到下载队列",
|
"OverrideAndAddToDownloadClient": "覆盖并添加到下载队列",
|
||||||
"OverrideGrabModalTitle": "覆盖并抓取 - {title}",
|
"OverrideGrabModalTitle": "覆盖并抓取 - {title}",
|
||||||
@@ -755,7 +755,7 @@
|
|||||||
"IndexerFileListSettingsFreeleechOnlyHelpText": "只搜索免费发布",
|
"IndexerFileListSettingsFreeleechOnlyHelpText": "只搜索免费发布",
|
||||||
"IndexerFileListSettingsUsernameHelpText": "网站用户名",
|
"IndexerFileListSettingsUsernameHelpText": "网站用户名",
|
||||||
"IndexerBeyondHDSettingsRefundOnlyHelpText": "Search refund only",
|
"IndexerBeyondHDSettingsRefundOnlyHelpText": "Search refund only",
|
||||||
"DownloadClientUTorrentProviderMessage": "由于uTorrent以加密软件、恶意软件和广告而闻名,我们建议切换到更好的客户端,例如qBittorrent、Deluge或ruTorrent。",
|
"DownloadClientUTorrentProviderMessage": "uTorrent 曾经含有挖矿行为、恶意软件和广告,我们强烈建议你选择其他客户端。",
|
||||||
"IndexerId": "索引器",
|
"IndexerId": "索引器",
|
||||||
"IndexerSettingsPasskey": "通行密钥",
|
"IndexerSettingsPasskey": "通行密钥",
|
||||||
"IndexerBeyondHDSettingsRefundOnly": "只读",
|
"IndexerBeyondHDSettingsRefundOnly": "只读",
|
||||||
@@ -763,5 +763,45 @@
|
|||||||
"IndexerBeyondHDSettingsRssKeyHelpText": "来自网站的API密钥(在我的安全 => API密钥)",
|
"IndexerBeyondHDSettingsRssKeyHelpText": "来自网站的API密钥(在我的安全 => API密钥)",
|
||||||
"IndexerHDBitsSettingsFreeleechOnlyHelpText": "只搜索免费发布",
|
"IndexerHDBitsSettingsFreeleechOnlyHelpText": "只搜索免费发布",
|
||||||
"IndexerHDBitsSettingsOrigins": "原始",
|
"IndexerHDBitsSettingsOrigins": "原始",
|
||||||
"IndexerPassThePopcornSettingsApiUserHelpText": "这些设置位于 PassThePopcorn 安全设置中(编辑个人资料 > 安全,Edit Profile > Security)。"
|
"IndexerPassThePopcornSettingsApiUserHelpText": "这些设置位于 PassThePopcorn 安全设置中(编辑个人资料 > 安全,Edit Profile > Security)。",
|
||||||
|
"IndexerBeyondHDSettingsSearchTypesHelpText": "选择你感兴趣的版本类型。若无则使用所有可选项。",
|
||||||
|
"IndexerFileListSettingsPasskeyHelpText": "网站密钥(下载客户端中跟踪器链接显示的由字母和数字构成的字符串)",
|
||||||
|
"IndexerGazelleGamesSettingsApiKeyHelpText": "来自网站的 API 密钥(在设置 => 访问设置中)",
|
||||||
|
"IndexerGazelleGamesSettingsApiKeyHelpTextWarning": "必须具有用户和种子权限",
|
||||||
|
"IndexerGazelleGamesSettingsSearchGroupNames": "搜索群组名",
|
||||||
|
"IndexerGazelleGamesSettingsSearchGroupNamesHelpText": "根据群组名搜索版本",
|
||||||
|
"IndexerHDBitsSettingsPasskeyHelpText": "用户详情中的密钥",
|
||||||
|
"IndexerHDBitsSettingsUseFilenames": "使用文件名",
|
||||||
|
"IndexerHDBitsSettingsUseFilenamesHelpText": "勾选此选项如果你想将种子文件名作为发布资源的标题",
|
||||||
|
"IndexerIPTorrentsSettingsCookieUserAgentHelpText": "浏览器中 cookie 所关联的 User-Agent",
|
||||||
|
"IndexerMTeamTpSettingsApiKeyHelpText": "站点中的 API 密钥(在用户控制面板 => 安全 => 实验室中)",
|
||||||
|
"IndexerNebulanceSettingsApiKeyHelpText": "在用户设置 > API 密钥中的API 密钥。密钥必须有列出和下载的权限",
|
||||||
|
"IndexerNewznabSettingsApiKeyHelpText": "站点 API 密钥",
|
||||||
|
"IndexerSettingsCookieHelpText": "站点 Cookie",
|
||||||
|
"IndexerSettingsFreeleechOnly": "仅免流资源",
|
||||||
|
"IndexerSettingsGrabLimit": "抓取上限",
|
||||||
|
"IndexerSettingsGrabLimitHelpText": "在该时间单位内,{appName} 对此站点所允许的最大抓取数",
|
||||||
|
"IndexerSettingsLimitsUnit": "限制单位",
|
||||||
|
"IndexerSettingsLimitsUnitHelpText": "每个索引器计算上限的时间单位",
|
||||||
|
"IndexerSettingsPreferMagnetUrl": "磁力链接优先",
|
||||||
|
"IndexerSettingsPreferMagnetUrlHelpText": "若开启,该索引器将优先使用磁力链接抓取,而种子链接作为备用",
|
||||||
|
"IndexerSettingsQueryLimit": "请求上限",
|
||||||
|
"IndexerSettingsQueryLimitHelpText": "在该时间单位内,{appName} 对此站点所允许的最大请求数",
|
||||||
|
"IndexerIPTorrentsSettingsCookieUserAgent": "Cookie User-Agent",
|
||||||
|
"IndexerNewznabSettingsVipExpirationHelpText": "输入 VIP 到期日期 (yyyy-mm-dd) 或留空,{appName} 将在到期前 1 星期发送通知",
|
||||||
|
"IndexerNzbIndexSettingsApiKeyHelpText": "站点 API 密钥",
|
||||||
|
"IndexerOrpheusSettingsApiKeyHelpText": "站点 API 密钥(在设置 => 访问设置中)",
|
||||||
|
"IndexerPassThePopcornSettingsApiKeyHelpText": "站点 API 密钥",
|
||||||
|
"IndexerPassThePopcornSettingsGoldenPopcornOnly": "仅 Golden Popcorn",
|
||||||
|
"IndexerPassThePopcornSettingsGoldenPopcornOnlyHelpText": "仅搜索 Golden Popcorn 发布的资源",
|
||||||
|
"IndexerRedactedSettingsApiKeyHelpText": "站点 API 密钥(在设置 => 访问设置中)",
|
||||||
|
"IndexerSettingsBaseUrl": "基础链接",
|
||||||
|
"IndexerSettingsBaseUrlHelpText": "选择 {appName} 请求该站点所使用的基础链接",
|
||||||
|
"IndexerTorrentSyndikatSettingsApiKeyHelpText": "站点 API 密钥",
|
||||||
|
"NoApplicationsFound": "找不到程序",
|
||||||
|
"Open": "打开",
|
||||||
|
"PreferMagnetUrl": "磁力链接优先",
|
||||||
|
"PreferMagnetUrlHelpText": "若开启,该索引器将优先使用磁力链接,种子链接作为备用",
|
||||||
|
"ProwlarrDownloadClientsAlert": "如果你想直接在 {appName} 中搜索,你需要添加下载管理器,否则你不需要在此添加。你的程序中的搜索行为将使用它自己的下载管理器配置。",
|
||||||
|
"ProwlarrDownloadClientsInAppOnlyAlert": "下载管理器仅对 {appName} 中搜索有效,而不会同步到其他程序中。目前没有添加此功能的计划。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,9 @@
|
|||||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||||
<PackageReference Include="Polly" Version="8.6.4" />
|
<PackageReference Include="Polly" Version="8.6.4" />
|
||||||
<PackageReference Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
|
<PackageReference Include="FluentMigrator.Runner.Core" Version="6.2.0" />
|
||||||
<PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
|
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="6.2.0" />
|
||||||
<PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
|
<PackageReference Include="FluentMigrator.Runner.SQLite" Version="6.2.0" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="8.0.19" />
|
<PackageReference Include="System.Drawing.Common" Version="8.0.19" />
|
||||||
<PackageReference Include="System.Memory" Version="4.6.3" />
|
<PackageReference Include="System.Memory" Version="4.6.3" />
|
||||||
<PackageReference Include="System.ServiceModel.Syndication" Version="8.0.0" />
|
<PackageReference Include="System.ServiceModel.Syndication" Version="8.0.0" />
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ namespace NzbDrone.Test.Common.AutoMoq
|
|||||||
|
|
||||||
if (behavior != MockBehavior.Default && mock.Behavior == MockBehavior.Default)
|
if (behavior != MockBehavior.Default && mock.Behavior == MockBehavior.Default)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Unable to change be behaviour of a an existing mock.");
|
throw new InvalidOperationException("Unable to change be behaviour of an existing mock.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return mock;
|
return mock;
|
||||||
@@ -139,7 +139,7 @@ namespace NzbDrone.Test.Common.AutoMoq
|
|||||||
|
|
||||||
LoadPlatformLibrary();
|
LoadPlatformLibrary();
|
||||||
|
|
||||||
AssemblyLoader.RegisterSQLiteResolver();
|
AssemblyLoader.RegisterNativeResolver(new[] { "System.Data.SQLite", "Prowlarr.Core" });
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mock<T> TheRegisteredMockForThisType<T>(Type type)
|
private Mock<T> TheRegisteredMockForThisType<T>(Type type)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace Prowlarr.Api.V1.Search
|
|||||||
public string Type { get; set; }
|
public string Type { get; set; }
|
||||||
public List<int> IndexerIds { get; set; }
|
public List<int> IndexerIds { get; set; }
|
||||||
public List<int> Categories { get; set; }
|
public List<int> Categories { get; set; }
|
||||||
public int Limit { get; set; }
|
public int? Limit { get; set; }
|
||||||
public int Offset { get; set; }
|
public int? Offset { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
_backupService = backupService;
|
_backupService = backupService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Map(string resourceUrl)
|
protected override string FolderPath => _backupService.GetBackupFolder().TrimEnd(Path.DirectorySeparatorChar);
|
||||||
|
|
||||||
|
protected override string MapPath(string resourceUrl)
|
||||||
{
|
{
|
||||||
var path = resourceUrl.Replace("/backup/", "").Replace('/', Path.DirectorySeparatorChar);
|
var path = resourceUrl.Replace("/backup/", "").Replace('/', Path.DirectorySeparatorChar);
|
||||||
|
|
||||||
return Path.Combine(_backupService.GetBackupFolder(), path);
|
return Path.Combine(FolderPath, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool CanHandle(string resourceUrl)
|
public override bool CanHandle(string resourceUrl)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
@@ -6,29 +6,29 @@ using NzbDrone.Core.Configuration;
|
|||||||
|
|
||||||
namespace Prowlarr.Http.Frontend.Mappers
|
namespace Prowlarr.Http.Frontend.Mappers
|
||||||
{
|
{
|
||||||
public class BrowserConfig : StaticResourceMapperBase
|
public class BrowserConfig : UrlBaseReplacementResourceMapperBase
|
||||||
{
|
{
|
||||||
private readonly IAppFolderInfo _appFolderInfo;
|
private readonly IAppFolderInfo _appFolderInfo;
|
||||||
private readonly IConfigFileProvider _configFileProvider;
|
private readonly IConfigFileProvider _configFileProvider;
|
||||||
|
|
||||||
public BrowserConfig(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger)
|
public BrowserConfig(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger)
|
||||||
: base(diskProvider, logger)
|
: base(diskProvider, configFileProvider, logger)
|
||||||
{
|
{
|
||||||
_appFolderInfo = appFolderInfo;
|
_appFolderInfo = appFolderInfo;
|
||||||
_configFileProvider = configFileProvider;
|
_configFileProvider = configFileProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Map(string resourceUrl)
|
protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder);
|
||||||
{
|
protected override string FilePath => Path.Combine(FolderPath, "Content", "browserconfig.xml");
|
||||||
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
|
|
||||||
path = path.Trim(Path.DirectorySeparatorChar);
|
|
||||||
|
|
||||||
return Path.ChangeExtension(Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path), "xml");
|
protected override string MapPath(string resourceUrl)
|
||||||
|
{
|
||||||
|
return FilePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool CanHandle(string resourceUrl)
|
public override bool CanHandle(string resourceUrl)
|
||||||
{
|
{
|
||||||
return resourceUrl.StartsWith("/content/images/icons/browserconfig");
|
return resourceUrl.StartsWith("/Content/browserconfig");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
|
|
||||||
var mapper = _diskMappers.Single(m => m.CanHandle(resourceUrl));
|
var mapper = _diskMappers.Single(m => m.CanHandle(resourceUrl));
|
||||||
var pathToFile = mapper.Map(resourceUrl);
|
var pathToFile = mapper.Map(resourceUrl);
|
||||||
|
|
||||||
|
if (pathToFile == null)
|
||||||
|
{
|
||||||
|
return resourceUrl;
|
||||||
|
}
|
||||||
|
|
||||||
var hash = _hashProvider.ComputeMd5(pathToFile).ToBase64();
|
var hash = _hashProvider.ComputeMd5(pathToFile).ToBase64();
|
||||||
|
|
||||||
return resourceUrl + "?h=" + hash.Trim('=');
|
return resourceUrl + "?h=" + hash.Trim('=');
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
_configFileProvider = configFileProvider;
|
_configFileProvider = configFileProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Map(string resourceUrl)
|
protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder);
|
||||||
|
|
||||||
|
protected override string MapPath(string resourceUrl)
|
||||||
{
|
{
|
||||||
var fileName = "favicon.ico";
|
var fileName = "favicon.ico";
|
||||||
|
|
||||||
@@ -29,7 +31,7 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
|
|
||||||
var path = Path.Combine("Content", "Images", "Icons", fileName);
|
var path = Path.Combine("Content", "Images", "Icons", fileName);
|
||||||
|
|
||||||
return Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path);
|
return Path.Combine(FolderPath, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool CanHandle(string resourceUrl)
|
public override bool CanHandle(string resourceUrl)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Text.RegularExpressions;
|
|||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
|
||||||
namespace Prowlarr.Http.Frontend.Mappers
|
namespace Prowlarr.Http.Frontend.Mappers
|
||||||
{
|
{
|
||||||
@@ -13,19 +14,22 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
private readonly Lazy<ICacheBreakerProvider> _cacheBreakProviderFactory;
|
private readonly Lazy<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 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 _urlBase;
|
||||||
private string _generatedContent;
|
private string _generatedContent;
|
||||||
|
|
||||||
protected HtmlMapperBase(IDiskProvider diskProvider,
|
protected HtmlMapperBase(IDiskProvider diskProvider,
|
||||||
|
IConfigFileProvider configFileProvider,
|
||||||
Lazy<ICacheBreakerProvider> cacheBreakProviderFactory,
|
Lazy<ICacheBreakerProvider> cacheBreakProviderFactory,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(diskProvider, logger)
|
: base(diskProvider, logger)
|
||||||
{
|
{
|
||||||
_diskProvider = diskProvider;
|
_diskProvider = diskProvider;
|
||||||
_cacheBreakProviderFactory = cacheBreakProviderFactory;
|
_cacheBreakProviderFactory = cacheBreakProviderFactory;
|
||||||
|
|
||||||
|
_urlBase = configFileProvider.UrlBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected string HtmlPath;
|
protected abstract string HtmlPath { get; }
|
||||||
protected string UrlBase;
|
|
||||||
|
|
||||||
protected override Stream GetContentStream(string filePath)
|
protected override Stream GetContentStream(string filePath)
|
||||||
{
|
{
|
||||||
@@ -62,10 +66,10 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
url = cacheBreakProvider.AddCacheBreakerToPath(match.Groups["path"].Value);
|
url = cacheBreakProvider.AddCacheBreakerToPath(match.Groups["path"].Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $"{match.Groups["attribute"].Value}=\"{UrlBase}{url}\"";
|
return $"{match.Groups["attribute"].Value}=\"{_urlBase}{url}\"";
|
||||||
});
|
});
|
||||||
|
|
||||||
text = text.Replace("__URL_BASE__", UrlBase);
|
text = text.Replace("__URL_BASE__", _urlBase);
|
||||||
|
|
||||||
_generatedContent = text;
|
_generatedContent = text;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
{
|
{
|
||||||
string Map(string resourceUrl);
|
string Map(string resourceUrl);
|
||||||
bool CanHandle(string resourceUrl);
|
bool CanHandle(string resourceUrl);
|
||||||
Task<FileStreamResult> GetResponse(string resourceUrl);
|
Task<IActionResult> GetResponse(string resourceUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
{
|
{
|
||||||
public class IndexHtmlMapper : HtmlMapperBase
|
public class IndexHtmlMapper : HtmlMapperBase
|
||||||
{
|
{
|
||||||
|
private readonly IAppFolderInfo _appFolderInfo;
|
||||||
private readonly IConfigFileProvider _configFileProvider;
|
private readonly IConfigFileProvider _configFileProvider;
|
||||||
|
|
||||||
public IndexHtmlMapper(IAppFolderInfo appFolderInfo,
|
public IndexHtmlMapper(IAppFolderInfo appFolderInfo,
|
||||||
@@ -16,15 +17,16 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
IConfigFileProvider configFileProvider,
|
IConfigFileProvider configFileProvider,
|
||||||
Lazy<ICacheBreakerProvider> cacheBreakProviderFactory,
|
Lazy<ICacheBreakerProvider> cacheBreakProviderFactory,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(diskProvider, cacheBreakProviderFactory, logger)
|
: base(diskProvider, configFileProvider, cacheBreakProviderFactory, logger)
|
||||||
{
|
{
|
||||||
|
_appFolderInfo = appFolderInfo;
|
||||||
_configFileProvider = configFileProvider;
|
_configFileProvider = configFileProvider;
|
||||||
|
|
||||||
HtmlPath = Path.Combine(appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, "index.html");
|
|
||||||
UrlBase = configFileProvider.UrlBase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Map(string resourceUrl)
|
protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder);
|
||||||
|
protected override string HtmlPath => Path.Combine(FolderPath, "index.html");
|
||||||
|
|
||||||
|
protected override string MapPath(string resourceUrl)
|
||||||
{
|
{
|
||||||
return HtmlPath;
|
return HtmlPath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,14 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
_appFolderInfo = appFolderInfo;
|
_appFolderInfo = appFolderInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Map(string resourceUrl)
|
protected override string FolderPath => _appFolderInfo.GetLogFolder();
|
||||||
|
|
||||||
|
protected override string MapPath(string resourceUrl)
|
||||||
{
|
{
|
||||||
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
|
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
|
||||||
path = Path.GetFileName(path);
|
path = Path.GetFileName(path);
|
||||||
|
|
||||||
return Path.Combine(_appFolderInfo.GetLogFolder(), path);
|
return Path.Combine(FolderPath, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool CanHandle(string resourceUrl)
|
public override bool CanHandle(string resourceUrl)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
{
|
{
|
||||||
public class LoginHtmlMapper : HtmlMapperBase
|
public class LoginHtmlMapper : HtmlMapperBase
|
||||||
{
|
{
|
||||||
|
private readonly IAppFolderInfo _appFolderInfo;
|
||||||
private readonly IConfigFileProvider _configFileProvider;
|
private readonly IConfigFileProvider _configFileProvider;
|
||||||
|
|
||||||
public LoginHtmlMapper(IAppFolderInfo appFolderInfo,
|
public LoginHtmlMapper(IAppFolderInfo appFolderInfo,
|
||||||
@@ -16,14 +17,16 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
Lazy<ICacheBreakerProvider> cacheBreakProviderFactory,
|
Lazy<ICacheBreakerProvider> cacheBreakProviderFactory,
|
||||||
IConfigFileProvider configFileProvider,
|
IConfigFileProvider configFileProvider,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(diskProvider, cacheBreakProviderFactory, logger)
|
: base(diskProvider, configFileProvider, cacheBreakProviderFactory, logger)
|
||||||
{
|
{
|
||||||
|
_appFolderInfo = appFolderInfo;
|
||||||
_configFileProvider = configFileProvider;
|
_configFileProvider = configFileProvider;
|
||||||
HtmlPath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "login.html");
|
|
||||||
UrlBase = configFileProvider.UrlBase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Map(string resourceUrl)
|
protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder);
|
||||||
|
protected override string HtmlPath => Path.Combine(FolderPath, "login.html");
|
||||||
|
|
||||||
|
protected override string MapPath(string resourceUrl)
|
||||||
{
|
{
|
||||||
return HtmlPath;
|
return HtmlPath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
@@ -6,29 +6,47 @@ using NzbDrone.Core.Configuration;
|
|||||||
|
|
||||||
namespace Prowlarr.Http.Frontend.Mappers
|
namespace Prowlarr.Http.Frontend.Mappers
|
||||||
{
|
{
|
||||||
public class ManifestMapper : StaticResourceMapperBase
|
public class ManifestMapper : UrlBaseReplacementResourceMapperBase
|
||||||
{
|
{
|
||||||
private readonly IAppFolderInfo _appFolderInfo;
|
private readonly IAppFolderInfo _appFolderInfo;
|
||||||
private readonly IConfigFileProvider _configFileProvider;
|
private readonly IConfigFileProvider _configFileProvider;
|
||||||
|
|
||||||
|
private string _generatedContent;
|
||||||
|
|
||||||
public ManifestMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger)
|
public ManifestMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger)
|
||||||
: base(diskProvider, logger)
|
: base(diskProvider, configFileProvider, logger)
|
||||||
{
|
{
|
||||||
_appFolderInfo = appFolderInfo;
|
_appFolderInfo = appFolderInfo;
|
||||||
_configFileProvider = configFileProvider;
|
_configFileProvider = configFileProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Map(string resourceUrl)
|
protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder);
|
||||||
{
|
protected override string FilePath => Path.Combine(FolderPath, "Content", "manifest.json");
|
||||||
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
|
|
||||||
path = path.Trim(Path.DirectorySeparatorChar);
|
|
||||||
|
|
||||||
return Path.ChangeExtension(Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path), "json");
|
protected override string MapPath(string resourceUrl)
|
||||||
|
{
|
||||||
|
return FilePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool CanHandle(string resourceUrl)
|
public override bool CanHandle(string resourceUrl)
|
||||||
{
|
{
|
||||||
return resourceUrl.StartsWith("/Content/Images/Icons/manifest");
|
return resourceUrl.StartsWith("/Content/manifest");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string GetFileText()
|
||||||
|
{
|
||||||
|
if (RuntimeInfo.IsProduction && _generatedContent != null)
|
||||||
|
{
|
||||||
|
return _generatedContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = base.GetFileText();
|
||||||
|
|
||||||
|
text = text.Replace("__INSTANCE_NAME__", _configFileProvider.InstanceName);
|
||||||
|
|
||||||
|
_generatedContent = text;
|
||||||
|
|
||||||
|
return _generatedContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
_diskProvider = diskProvider;
|
_diskProvider = diskProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Map(string resourceUrl)
|
protected override string FolderPath => Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover");
|
||||||
|
|
||||||
|
protected override string MapPath(string resourceUrl)
|
||||||
{
|
{
|
||||||
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
|
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
|
||||||
path = path.Trim(Path.DirectorySeparatorChar);
|
path = path.Trim(Path.DirectorySeparatorChar);
|
||||||
|
|||||||
@@ -18,11 +18,13 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
_configFileProvider = configFileProvider;
|
_configFileProvider = configFileProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Map(string resourceUrl)
|
protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder);
|
||||||
|
|
||||||
|
protected override string MapPath(string resourceUrl)
|
||||||
{
|
{
|
||||||
var path = Path.Combine("Content", "robots.txt");
|
var path = Path.Combine("Content", "robots.txt");
|
||||||
|
|
||||||
return Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path);
|
return Path.Combine(FolderPath, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool CanHandle(string resourceUrl)
|
public override bool CanHandle(string resourceUrl)
|
||||||
|
|||||||
@@ -18,20 +18,22 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
_configFileProvider = configFileProvider;
|
_configFileProvider = configFileProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Map(string resourceUrl)
|
protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder);
|
||||||
|
|
||||||
|
protected override string MapPath(string resourceUrl)
|
||||||
{
|
{
|
||||||
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
|
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
|
||||||
path = path.Trim(Path.DirectorySeparatorChar);
|
path = path.Trim(Path.DirectorySeparatorChar);
|
||||||
|
|
||||||
return Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path);
|
return Path.Combine(FolderPath, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool CanHandle(string resourceUrl)
|
public override bool CanHandle(string resourceUrl)
|
||||||
{
|
{
|
||||||
resourceUrl = resourceUrl.ToLowerInvariant();
|
resourceUrl = resourceUrl.ToLowerInvariant();
|
||||||
|
|
||||||
if (resourceUrl.StartsWith("/content/images/icons/manifest") ||
|
if (resourceUrl.StartsWith("/content/manifest") ||
|
||||||
resourceUrl.StartsWith("/content/images/icons/browserconfig"))
|
resourceUrl.StartsWith("/content/browserconfig"))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,14 +27,28 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
_caseSensitive = RuntimeInfo.IsProduction ? DiskProviderBase.PathStringComparison : StringComparison.OrdinalIgnoreCase;
|
_caseSensitive = RuntimeInfo.IsProduction ? DiskProviderBase.PathStringComparison : StringComparison.OrdinalIgnoreCase;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract string Map(string resourceUrl);
|
protected abstract string FolderPath { get; }
|
||||||
|
protected abstract string MapPath(string resourceUrl);
|
||||||
|
|
||||||
public abstract bool CanHandle(string resourceUrl);
|
public abstract bool CanHandle(string resourceUrl);
|
||||||
|
|
||||||
public Task<FileStreamResult> GetResponse(string resourceUrl)
|
public string Map(string resourceUrl)
|
||||||
|
{
|
||||||
|
var filePath = Path.GetFullPath(MapPath(resourceUrl));
|
||||||
|
var parentPath = Path.GetFullPath(FolderPath) + Path.DirectorySeparatorChar;
|
||||||
|
|
||||||
|
return filePath.StartsWith(parentPath) ? filePath : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IActionResult> GetResponse(string resourceUrl)
|
||||||
{
|
{
|
||||||
var filePath = Map(resourceUrl);
|
var filePath = Map(resourceUrl);
|
||||||
|
|
||||||
|
if (filePath == null)
|
||||||
|
{
|
||||||
|
return Task.FromResult<IActionResult>(null);
|
||||||
|
}
|
||||||
|
|
||||||
if (_diskProvider.FileExists(filePath, _caseSensitive))
|
if (_diskProvider.FileExists(filePath, _caseSensitive))
|
||||||
{
|
{
|
||||||
if (!_mimeTypeProvider.TryGetContentType(filePath, out var contentType))
|
if (!_mimeTypeProvider.TryGetContentType(filePath, out var contentType))
|
||||||
@@ -42,7 +56,7 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
contentType = "application/octet-stream";
|
contentType = "application/octet-stream";
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(new FileStreamResult(GetContentStream(filePath), new MediaTypeHeaderValue(contentType)
|
return Task.FromResult<IActionResult>(new FileStreamResult(GetContentStream(filePath), new MediaTypeHeaderValue(contentType)
|
||||||
{
|
{
|
||||||
Encoding = contentType == "text/plain" ? Encoding.UTF8 : null
|
Encoding = contentType == "text/plain" ? Encoding.UTF8 : null
|
||||||
}));
|
}));
|
||||||
@@ -50,7 +64,7 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
|
|
||||||
_logger.Warn("File {0} not found", filePath);
|
_logger.Warn("File {0} not found", filePath);
|
||||||
|
|
||||||
return Task.FromResult<FileStreamResult>(null);
|
return Task.FromResult<IActionResult>(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual Stream GetContentStream(string filePath)
|
protected virtual Stream GetContentStream(string filePath)
|
||||||
|
|||||||
@@ -16,12 +16,14 @@ namespace Prowlarr.Http.Frontend.Mappers
|
|||||||
_appFolderInfo = appFolderInfo;
|
_appFolderInfo = appFolderInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Map(string resourceUrl)
|
protected override string FolderPath => _appFolderInfo.GetUpdateLogFolder();
|
||||||
|
|
||||||
|
protected override string MapPath(string resourceUrl)
|
||||||
{
|
{
|
||||||
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
|
var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar);
|
||||||
path = Path.GetFileName(path);
|
path = Path.GetFileName(path);
|
||||||
|
|
||||||
return Path.Combine(_appFolderInfo.GetUpdateLogFolder(), path);
|
return Path.Combine(FolderPath, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool CanHandle(string resourceUrl)
|
public override bool CanHandle(string resourceUrl)
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System.IO;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
|
||||||
|
namespace Prowlarr.Http.Frontend.Mappers
|
||||||
|
{
|
||||||
|
public abstract class UrlBaseReplacementResourceMapperBase : StaticResourceMapperBase
|
||||||
|
{
|
||||||
|
private readonly IDiskProvider _diskProvider;
|
||||||
|
private readonly string _urlBase;
|
||||||
|
|
||||||
|
private string _generatedContent;
|
||||||
|
|
||||||
|
public UrlBaseReplacementResourceMapperBase(IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger)
|
||||||
|
: base(diskProvider, logger)
|
||||||
|
{
|
||||||
|
_diskProvider = diskProvider;
|
||||||
|
_urlBase = configFileProvider.UrlBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract string FilePath { get; }
|
||||||
|
|
||||||
|
protected override string MapPath(string resourceUrl)
|
||||||
|
{
|
||||||
|
return FilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Stream GetContentStream(string filePath)
|
||||||
|
{
|
||||||
|
var text = GetFileText();
|
||||||
|
|
||||||
|
var stream = new MemoryStream();
|
||||||
|
var writer = new StreamWriter(stream);
|
||||||
|
writer.Write(text);
|
||||||
|
writer.Flush();
|
||||||
|
stream.Position = 0;
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual string GetFileText()
|
||||||
|
{
|
||||||
|
if (RuntimeInfo.IsProduction && _generatedContent != null)
|
||||||
|
{
|
||||||
|
return _generatedContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = _diskProvider.ReadAllText(FilePath);
|
||||||
|
|
||||||
|
text = text.Replace("__URL_BASE__", _urlBase);
|
||||||
|
|
||||||
|
_generatedContent = text;
|
||||||
|
|
||||||
|
return _generatedContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Cors;
|
using Microsoft.AspNetCore.Cors;
|
||||||
@@ -16,6 +17,7 @@ namespace Prowlarr.Http.Frontend
|
|||||||
{
|
{
|
||||||
private readonly IEnumerable<IMapHttpRequestsToDisk> _requestMappers;
|
private readonly IEnumerable<IMapHttpRequestsToDisk> _requestMappers;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
private static readonly Regex InvalidPathRegex = new(@"([\/\\]|%2f|%5c)\.\.|\.\.([\/\\]|%2f|%5c)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
|
||||||
public StaticResourceController(IEnumerable<IMapHttpRequestsToDisk> requestMappers,
|
public StaticResourceController(IEnumerable<IMapHttpRequestsToDisk> requestMappers,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
@@ -50,6 +52,11 @@ namespace Prowlarr.Http.Frontend
|
|||||||
{
|
{
|
||||||
path = "/" + (path ?? "");
|
path = "/" + (path ?? "");
|
||||||
|
|
||||||
|
if (InvalidPathRegex.IsMatch(path))
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
var mapper = _requestMappers.SingleOrDefault(m => m.CanHandle(path));
|
var mapper = _requestMappers.SingleOrDefault(m => m.CanHandle(path));
|
||||||
|
|
||||||
if (mapper != null)
|
if (mapper != null)
|
||||||
@@ -58,7 +65,7 @@ namespace Prowlarr.Http.Frontend
|
|||||||
|
|
||||||
if (result != null)
|
if (result != null)
|
||||||
{
|
{
|
||||||
if (result.ContentType == "text/html")
|
if ((result as FileResult)?.ContentType == "text/html")
|
||||||
{
|
{
|
||||||
Response.Headers.DisableCache();
|
Response.Headers.DisableCache();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2289,9 +2289,9 @@ camelcase@^5.3.1:
|
|||||||
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001663, caniuse-lite@^1.0.30001716:
|
caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001663, caniuse-lite@^1.0.30001716:
|
||||||
version "1.0.30001718"
|
version "1.0.30001782"
|
||||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz"
|
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001782.tgz"
|
||||||
integrity sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==
|
integrity sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==
|
||||||
|
|
||||||
chalk@^2.4.1, chalk@^2.4.2:
|
chalk@^2.4.1, chalk@^2.4.2:
|
||||||
version "2.4.2"
|
version "2.4.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user