Compare commits

...

46 Commits

Author SHA1 Message Date
Qstick
cec304a0be Don't require user agent for IPTorrents 2022-07-04 18:13:18 -05:00
Qstick
06f3c8e151 Fixed: (Applications) ApiPath can be null from -arr in some cases 2022-07-04 14:24:32 -05:00
Qstick
91eb65bd6c ProtectionService Test Fixture 2022-07-04 13:02:25 -05:00
Qstick
832080cb36 Fixed: Lidarr null ref when building indexer for sync
Fixes PROWLARR-B5Y
2022-07-04 12:59:58 -05:00
Qstick
f9c731627f Fixed: Lidarr null ref when building indexer for sync
Fixes PROWLARR-856
2022-07-04 12:58:50 -05:00
Qstick
83344fb6f4 Double MultipartBodyLengthLimit for Backup Restore to 256MB 2022-07-04 11:21:14 -05:00
Qstick
59b7435820 Fixed: (IPTorrents) Allow UA override for CF
Fixes #842
Fixes #809
2022-07-03 17:23:47 -05:00
bakerboy448
d2c1ffa761 Fixed: Log Cleanse Indexer Response Logic and Test Cases 2022-07-03 15:05:33 -05:00
Qstick
5436d4f800 Fixed: Set update executable permissions correctly 2022-07-03 12:32:41 -05:00
Qstick
86fe19a5dd Fixed: Don't call for server notifications on event driven check
[common]
2022-07-03 12:26:02 -05:00
Qstick
473405ceeb Update file and folder handling methods from Radarr (#1051)
* Update file/folder handling methods from Radarr

* fixup!
2022-07-02 18:40:00 -05:00
Robin Dadswell
cac2729230 Running Integration Tests against Postgres Database (#838)
* Allow configuring postgres with environment variables

(cherry picked from commit 8439df78fea25656a9a1275d2a2fe3f0df0528c7)

* Fix process provider when environment variables alread exist

[common]

(cherry picked from commit 66e5b4025974e081c1406f01a860b1ac52949c22)

* First bash at running integration tests on postgres

(cherry picked from commit f950e80c7e4f9b088ec6a149386160eab83b61c3)

* Postgres integration tests running as part of the build pipeline

(cherry picked from commit 9ca8616f5098778e9b5e6ce09d2aa11224018fab)

* Fixed: Register PostgresOptions when running in utility mode

* fixup!

* fixup!

* fixup!

* fixup!

* fixup!

Co-authored-by: ta264 <ta264@users.noreply.github.com>
Co-authored-by: Qstick <qstick@gmail.com>
2022-07-02 17:48:10 -05:00
Robin Dadswell
9b1f9abfac Updated NLog Version (#7365) 2022-07-02 14:22:23 -05:00
Qstick
76285a8ccd Add additional link logging to DownloadService 2022-06-28 19:45:16 -05:00
Qstick
a6b499e4a5 Fixed: Correctly remove TorrentParadiseMl
Fixes #1046
2022-06-28 18:31:13 -05:00
Qstick
5ee95e3cc2 V6 Cardigann Changes (#1045)
* V6 Cardigann Changes

* fixup!

* !fixup range

* !fixup more cardigann tests
2022-06-27 20:39:15 -05:00
Mark McDowall
654d2dbad3 Sliding expiration for auth cookie and a little clean up 2022-06-26 11:19:10 -05:00
Qstick
67ae7e32df Bump version to 0.4.2 2022-06-26 10:50:01 -05:00
Qstick
d52516b700 Update Sentry to 3.18.0 2022-06-25 18:26:37 -05:00
Qstick
326a7b5e16 Update Swashbuckle to 6.3.1 2022-06-25 18:26:07 -05:00
Qstick
313a0b459a Bump dotnet to 6.0.6 2022-06-25 18:24:44 -05:00
Qstick
2ffe88bf6a Update AngleSharp to 0.17.0 2022-06-25 18:22:28 -05:00
Qstick
a311d13805 Remove ShowRSS C# Implementation 2022-06-25 18:06:40 -05:00
Qstick
0e2d15cb73 Swallow HTTP issues on analytics call 2022-06-25 16:19:55 -05:00
Qstick
d1949d24e0 Fix NullRef in analytics service 2022-06-25 16:12:50 -05:00
Qstick
cdb1f163f8 Bump version to 0.4.1 2022-06-25 13:05:54 -05:00
Qstick
580fc528e5 Fix Donation Links 2022-06-24 18:49:08 -05:00
Qstick
dfed229a1d Fix Tooltips in Dark Theme 2022-06-24 18:46:58 -05:00
bakerboy448
e76a255229 Fixed: (AnimeBytes) Cleanse Passkey from response
Fixes #1041
2022-06-24 09:54:36 -05:00
Qstick
a0b650e7a5 Fixed: (Cardigann) Use variables in keywordsfilters block
Fixes #1035
Fixes v5 TorrentLand
2022-06-23 22:22:30 -05:00
Qstick
7cf9fc6a4f New: (BeyondHD) Better status messages for failures
Closes #1028
2022-06-23 20:56:07 -05:00
Qstick
86f5768461 Fixed: VIP Healthcheck not triggered for expired indexers 2022-06-23 20:36:13 -05:00
ta264
479e29dde7 Use DryIoc for Automoqer, drop Unity dependency
(cherry picked from commit e3468daba04b52fbf41ce3004934a26b0220ec4f)
2022-06-22 10:57:36 +01:00
olly
761e15a476 New: Send description element in nab response 2022-06-21 09:16:07 -05:00
Davo1624
d3ffb7b490 (Filelist) Update help text for pass key (#1039) 2022-06-21 09:14:02 -05:00
Qstick
0db804b647 Fixed: (Exoticaz) Category parsing kills search/feed
Fixes #938
2022-06-20 21:39:20 -05:00
Qstick
4334e7eef1 New: (PassThePopcorn) Freeleech only option
Fixes #1014
2022-06-11 15:04:35 -05:00
Qstick
fbfb70a1bb Fixed: (Cardigann) Searching with nab Parent should also use Child categories
Fixes #1031
2022-06-11 14:22:09 -05:00
bakerboy448
59e990227d Fixed: Better Cleansing of Tracker Announce Keys
Fixed: Cleanse Notifiarr secret from URL in logs

(cherry picked from commit e6210aede6f7ead197e82572976bc0267d910d46)
(cherry picked from commit ec866082d44d299096112a6c7c232384b1f74505)
2022-06-11 13:42:32 -05:00
Servarr
f0abfae978 Automated API Docs update 2022-06-04 08:47:47 -05:00
Qstick
40e7f80be9 Update FE dev dependencies 2022-06-04 00:42:40 -05:00
ta264
3c913eac24 Ensure .Mono and .Windows projects have all dependencies in build output
Fixes development on linux
2022-05-31 05:35:16 +01:00
Qstick
95a2bd3d03 Fixed: (Gazelle) Parse grouptime as long or date
Closes #973
Fixes #773
Closes #1008
2022-05-19 21:58:35 -05:00
Qstick
d5abde98e1 Fixed: (ExoticaZ) Category Parsing
Fixes #938
2022-05-19 21:23:33 -05:00
Qstick
5d14d2c134 Fixed: Input options background color on mobile 2022-05-18 15:47:17 -04:00
gofaster
ed46c5c86d Fixed: Update AltHub API URL (#1010) 2022-05-18 12:36:16 -05:00
118 changed files with 8349 additions and 1874 deletions

View File

@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.4.0'
majorVersion: '0.4.2'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.201'
dotnetVersion: '6.0.301'
innoVersion: '6.2.0'
nodeVersion: '16.x'
windowsImage: 'windows-2022'
@@ -506,6 +506,58 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres
displayName: Unit Native LinuxCore with Postgres Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
artifactName: LinuxCoreTests
Prowlarr__Postgres__Host: 'localhost'
Prowlarr__Postgres__Port: '5432'
Prowlarr__Postgres__User: 'prowlarr'
Prowlarr__Postgres__Password: 'prowlarr'
pool:
vmImage: 'ubuntu-18.04'
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: $(artifactName)
targetPath: $(testsFolder)
- bash: find ${TESTSFOLDER} -name "Prowlarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- bash: |
docker run -d --name=postgres14 \
-e POSTGRES_PASSWORD=prowlarr \
-e POSTGRES_USER=prowlarr \
-p 5432:5432/tcp \
postgres:14
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
ls -lR ${TESTSFOLDER}
${TESTSFOLDER}/test.sh Linux Unit Test
displayName: Run Tests
- task: PublishTestResults@2
displayName: Publish Test Results
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres Unit Tests'
failTaskOnFailedTests: true
- stage: Integration
displayName: Integration
@@ -590,6 +642,67 @@ stages:
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres
displayName: Integration Native LinuxCore with Postgres Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
Prowlarr__Postgres__Host: 'localhost'
Prowlarr__Postgres__Port: '5432'
Prowlarr__Postgres__User: 'prowlarr'
Prowlarr__Postgres__Password: 'prowlarr'
pool:
vmImage: 'ubuntu-18.04'
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'LinuxCoreTests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
buildType: 'current'
artifactName: Packages
itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
mkdir -p ./bin/
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Prowlarr/. ./bin/
displayName: Move Package Contents
- bash: |
docker run -d --name=postgres14 \
-e POSTGRES_PASSWORD=prowlarr \
-e POSTGRES_USER=prowlarr \
-p 5432:5432/tcp \
postgres:14
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
${TESTSFOLDER}/test.sh Linux Integration Test
displayName: Run Integration Tests
- task: PublishTestResults@2
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_FreeBSD
displayName: Integration Native FreeBSD
dependsOn: Prepare

View File

@@ -78,7 +78,7 @@
border: 1px solid var(--inputBorderColor);
border-radius: 4px;
background-color: var(--white);
background-color: var(--inputBackgroundColor);
}
.loading {

View File

@@ -74,7 +74,7 @@ class PageHeader extends Component {
<IconButton
className={styles.donate}
name={icons.HEART}
to="https://opencollective.com/prowlarr"
to="https://prowlarr.com/donate"
size={14}
/>
<IconButton

View File

@@ -7,7 +7,7 @@
position: relative;
&.default {
background-color: var(--white);
background-color: var(--popoverBodyBackgroundColor);
box-shadow: 0 5px 10px var(--popoverShadowColor);
}

View File

@@ -24,6 +24,7 @@ const searchOptions = [
const seriesTokens = [
{ token: '{ImdbId:tt1234567}', example: 'tt12345' },
{ token: '{TvdbId:12345}', example: '12345' },
{ token: '{TmdbId:12345}', example: '12345' },
{ token: '{TvMazeId:12345}', example: '54321' },
{ token: '{Season:00}', example: '01' },
{ token: '{Episode:00}', example: '01' }

View File

@@ -168,10 +168,11 @@ module.exports = {
//
// Popover
popoverTitleBackgroundColor: '#f7f7f7',
popoverTitleBorderColor: '#ebebeb',
popoverTitleBackgroundColor: '#424242',
popoverTitleBorderColor: '#2a2a2a',
popoverBodyBackgroundColor: '#2a2a2a',
popoverShadowColor: 'rgba(0, 0, 0, 0.2)',
popoverArrowBorderColor: '#fff',
popoverArrowBorderColor: '#2a2a2a',
popoverTitleBackgroundInverseColor: '#595959',
popoverTitleBorderInverseColor: '#707070',

View File

@@ -170,6 +170,7 @@ module.exports = {
popoverTitleBackgroundColor: '#f7f7f7',
popoverTitleBorderColor: '#ebebeb',
popoverBodyBackgroundColor: '#e9e9e9',
popoverShadowColor: 'rgba(0, 0, 0, 0.2)',
popoverArrowBorderColor: '#fff',

View File

@@ -13,7 +13,7 @@ class Donations extends Component {
return (
<FieldSet legend={translate('Donations')}>
<div className={styles.logoContainer} title="Radarr">
<Link to="https://opencollective.com/radarr">
<Link to="https://radarr.video/donate">
<img
className={styles.logo}
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-radarr.png`}
@@ -21,7 +21,7 @@ class Donations extends Component {
</Link>
</div>
<div className={styles.logoContainer} title="Lidarr">
<Link to="https://opencollective.com/lidarr">
<Link to="https://lidarr.audio/donate">
<img
className={styles.logo}
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-lidarr.png`}
@@ -29,7 +29,7 @@ class Donations extends Component {
</Link>
</div>
<div className={styles.logoContainer} title="Readarr">
<Link to="https://opencollective.com/readarr">
<Link to="https://readarr.com/donate">
<img
className={styles.logo}
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-readarr.png`}
@@ -37,7 +37,7 @@ class Donations extends Component {
</Link>
</div>
<div className={styles.logoContainer} title="Prowlarr">
<Link to="https://opencollective.com/prowlarr">
<Link to="https://prowlarr.com/donate">
<img
className={styles.logo}
src={`${window.Prowlarr.urlBase}/Content/Images/Icons/logo-prowlarr.png`}

View File

@@ -30,7 +30,7 @@
"@fortawesome/free-regular-svg-icons": "6.1.1",
"@fortawesome/free-solid-svg-icons": "6.1.1",
"@fortawesome/react-fontawesome": "0.1.18",
"@microsoft/signalr": "6.0.3",
"@microsoft/signalr": "6.0.6",
"@sentry/browser": "6.19.2",
"@sentry/integrations": "6.19.2",
"chart.js": "3.7.1",
@@ -78,38 +78,38 @@
"reselect": "4.0.0"
},
"devDependencies": {
"@babel/core": "7.17.8",
"@babel/eslint-parser": "7.17.0",
"@babel/plugin-proposal-class-properties": "7.16.7",
"@babel/plugin-proposal-decorators": "7.17.8",
"@babel/plugin-proposal-export-default-from": "7.16.7",
"@babel/plugin-proposal-export-namespace-from": "7.16.7",
"@babel/plugin-proposal-function-sent": "7.16.7",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
"@babel/core": "7.18.2",
"@babel/eslint-parser": "7.18.2",
"@babel/plugin-proposal-class-properties": "7.17.12",
"@babel/plugin-proposal-decorators": "7.18.2",
"@babel/plugin-proposal-export-default-from": "7.17.12",
"@babel/plugin-proposal-export-namespace-from": "7.17.12",
"@babel/plugin-proposal-function-sent": "7.18.2",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.17.12",
"@babel/plugin-proposal-numeric-separator": "7.16.7",
"@babel/plugin-proposal-optional-chaining": "7.16.7",
"@babel/plugin-proposal-optional-chaining": "7.17.12",
"@babel/plugin-proposal-throw-expressions": "7.16.7",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.16.11",
"@babel/preset-react": "7.16.7",
"autoprefixer": "10.4.4",
"babel-loader": "8.2.4",
"@babel/preset-env": "7.18.2",
"@babel/preset-react": "7.17.12",
"autoprefixer": "10.4.7",
"babel-loader": "8.2.5",
"babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.21.1",
"core-js": "3.22.8",
"css-loader": "6.7.1",
"eslint": "8.11.0",
"eslint": "8.17.0",
"eslint-plugin-filenames": "1.3.2",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-react": "7.29.4",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-react": "7.30.0",
"eslint-plugin-simple-import-sort": "7.0.0",
"esprint": "3.3.0",
"esprint": "3.6.0",
"file-loader": "6.2.0",
"filemanager-webpack-plugin": "6.1.7",
"html-webpack-plugin": "5.5.0",
"loader-utils": "^3.0.0",
"mini-css-extract-plugin": "2.6.0",
"postcss": "8.4.12",
"postcss": "8.4.14",
"postcss-color-function": "4.1.0",
"postcss-loader": "6.2.1",
"postcss-mixins": "9.0.2",
@@ -121,10 +121,10 @@
"run-sequence": "2.2.1",
"streamqueue": "1.1.2",
"style-loader": "3.3.1",
"stylelint": "14.6.0",
"stylelint": "14.8.5",
"stylelint-order": "5.0.0",
"url-loader": "4.1.1",
"webpack": "5.70.0",
"webpack": "5.73.0",
"webpack-cli": "4.9.2",
"webpack-livereload-plugin": "3.0.2"
}

View File

@@ -94,7 +94,7 @@
<!-- Standard testing packages -->
<ItemGroup Condition="'$(TestProject)'=='true'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />

View File

@@ -44,7 +44,7 @@ namespace NzbDrone.Automation.Test
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger());
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
_runner.KillAll();
_runner.Start();

View File

@@ -10,6 +10,16 @@ namespace NzbDrone.Common.Test.DiskTests
public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject>
where TSubject : class, IDiskProvider
{
[Test]
public void writealltext_should_truncate_existing()
{
var file = GetTempFilePath();
Subject.WriteAllText(file, "A pretty long string");
Subject.WriteAllText(file, "A short string");
Subject.ReadAllText(file).Should().Be("A short string");
}
[Test]
[Retry(5)]
public void directory_exist_should_be_able_to_find_existing_folder()

View File

@@ -402,6 +402,40 @@ namespace NzbDrone.Common.Test.DiskTests
VerifyCopyFolder(source.FullName, destination.FullName);
}
[Test]
public void CopyFolder_should_detect_caseinsensitive_parents()
{
WindowsOnly();
WithRealDiskProvider();
var original = GetFilledTempFolder();
var root = new DirectoryInfo(GetTempFilePath());
var source = new DirectoryInfo(root.FullName + "A/series");
var destination = new DirectoryInfo(root.FullName + "a/series");
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
}
[Test]
public void CopyFolder_should_detect_caseinsensitive_folder()
{
WindowsOnly();
WithRealDiskProvider();
var original = GetFilledTempFolder();
var root = new DirectoryInfo(GetTempFilePath());
var source = new DirectoryInfo(root.FullName + "A/series");
var destination = new DirectoryInfo(root.FullName + "A/Series");
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Copy));
}
[Test]
public void CopyFolder_should_ignore_nfs_temp_file()
{
@@ -451,6 +485,42 @@ namespace NzbDrone.Common.Test.DiskTests
VerifyMoveFolder(original.FullName, source.FullName, destination.FullName);
}
[Test]
public void MoveFolder_should_detect_caseinsensitive_parents()
{
WindowsOnly();
WithRealDiskProvider();
var original = GetFilledTempFolder();
var root = new DirectoryInfo(GetTempFilePath());
var source = new DirectoryInfo(root.FullName + "A/series");
var destination = new DirectoryInfo(root.FullName + "a/series");
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
Assert.Throws<IOException>(() => Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Move));
}
[Test]
public void MoveFolder_should_rename_caseinsensitive_folder()
{
WindowsOnly();
WithRealDiskProvider();
var original = GetFilledTempFolder();
var root = new DirectoryInfo(GetTempFilePath());
var source = new DirectoryInfo(root.FullName + "A/series");
var destination = new DirectoryInfo(root.FullName + "A/Series");
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
Subject.TransferFolder(source.FullName, destination.FullName, TransferMode.Move);
source.FullName.GetActualCasing().Should().Be(destination.FullName);
}
[Test]
public void should_throw_if_destination_is_readonly()
{
@@ -553,6 +623,23 @@ namespace NzbDrone.Common.Test.DiskTests
VerifyCopyFolder(original.FullName, destination.FullName);
}
[Test]
public void MirrorFolder_should_handle_trailing_slash()
{
WithRealDiskProvider();
var original = GetFilledTempFolder();
var source = new DirectoryInfo(GetTempFilePath());
var destination = new DirectoryInfo(GetTempFilePath());
Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy);
var count = Subject.MirrorFolder(source.FullName + Path.DirectorySeparatorChar, destination.FullName);
count.Should().Equals(3);
VerifyCopyFolder(original.FullName, destination.FullName);
}
[Test]
public void TransferFolder_should_use_movefolder_if_on_same_mount()
{
@@ -752,6 +839,10 @@ namespace NzbDrone.Common.Test.DiskTests
.Setup(v => v.CreateFolder(It.IsAny<string>()))
.Callback<string>(v => Directory.CreateDirectory(v));
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.MoveFolder(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()))
.Callback<string, string, bool>((v, r, b) => Directory.Move(v, r));
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.DeleteFolder(It.IsAny<string>(), It.IsAny<bool>()))
.Callback<string, bool>((v, r) => Directory.Delete(v, r));

View File

@@ -28,14 +28,6 @@ namespace NzbDrone.Common.Test.DiskTests
Subject.GetAvailableSpace(Path.Combine(path, "invalidFolder")).Should().NotBe(0);
}
[Ignore("Docker")]
[Test]
public void should_be_able_to_check_space_on_ramdrive()
{
PosixOnly();
Subject.GetAvailableSpace("/run/").Should().NotBe(0);
}
[Ignore("Docker")]
[Test]
public void should_return_free_disk_space()
@@ -44,35 +36,6 @@ namespace NzbDrone.Common.Test.DiskTests
result.Should().BeGreaterThan(0);
}
[Test]
public void should_be_able_to_get_space_on_unc()
{
WindowsOnly();
var result = Subject.GetAvailableSpace(@"\\localhost\c$\Windows");
result.Should().BeGreaterThan(0);
}
[Test]
public void should_throw_if_drive_doesnt_exist()
{
WindowsOnly();
// Find a drive that doesn't exist.
for (char driveletter = 'Z'; driveletter > 'D'; driveletter--)
{
if (new DriveInfo(driveletter.ToString()).IsReady)
{
continue;
}
Assert.Throws<DirectoryNotFoundException>(() => Subject.GetAvailableSpace(driveletter + @":\NOT_A_REAL_PATH\DOES_NOT_EXIST".AsOsAgnostic()));
return;
}
Assert.Inconclusive("No drive available for testing.");
}
[Ignore("Docker")]
[Test]
public void should_be_able_to_get_space_on_folder_that_doesnt_exist()

View File

@@ -28,9 +28,12 @@ namespace NzbDrone.Common.Test.InstrumentationTests
// Indexer and Download Client Responses
// avistaz response
[TestCase(@"""download"":""https:\/\/avistaz.to\/rss\/download\/2b51db35e1910123321025a12b9933d2\/tb51db35e1910123321025a12b9933d2.torrent"",")]
[TestCase(@"""download"":""https://avistaz.to/rss/download/2b51db35e1910123321025a12b9933d2/tb51db35e1910123321025a12b9933d2.torrent"",")]
[TestCase(@",""info_hash"":""2b51db35e1910123321025a12b9933d2"",")]
// animebytes response
[TestCase(@"""Link"":""https://animebytes.tv/torrent/994064/download/tb51db35e1910123321025a12b9933d2"",")]
// danish bytes response
[TestCase(@",""rsskey"":""2b51db35e1910123321025a12b9933d2"",")]
[TestCase(@",""passkey"":""2b51db35e1910123321025a12b9933d2"",")]
@@ -77,20 +80,24 @@ namespace NzbDrone.Common.Test.InstrumentationTests
// Download Station
[TestCase(@"webapi/entry.cgi?api=(removed)&version=2&method=login&account=01233210&passwd=mySecret&format=sid&session=DownloadStation")]
// Tracker Responses
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
// BroadcastheNet
[TestCase(@"method: ""getTorrents"", ""params"": [ ""mySecret"",")]
[TestCase(@"getTorrents(""mySecret"", [asdfasdf], 100, 0)")]
[TestCase(@"""DownloadURL"":""https:\/\/broadcasthe.net\/torrents.php?action=download&id=123&authkey=mySecret&torrent_pass=mySecret""")]
[TestCase(@"""DownloadURL"":""https://broadcasthe.net/torrents.php?action=download&id=123&authkey=mySecret&torrent_pass=mySecret""")]
// Notifiarr
// Webhooks - Notifiarr
[TestCase(@"https://xxx.yyy/api/v1/notification/prowlarr/9pr04sg6-0123-3210-imav-eql2tyu8xyui")]
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
// RSS
[TestCase(@"<atom:link href = ""https://api.nzb.su/api?t=search&amp;extended=1&amp;cat=3030&apikey=mySecret&amp;q=Diggers"" rel=""self"" type=""application/rss+xml"" />")]
// Internal
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
public void should_clean_message(string message)
{

View File

@@ -170,7 +170,7 @@ namespace NzbDrone.Common.Test
var processStarted = new ManualResetEventSlim();
string suffix;
if (OsInfo.IsWindows || PlatformInfo.IsMono)
if (OsInfo.IsWindows)
{
suffix = ".exe";
}

View File

@@ -4,11 +4,13 @@ using DryIoc.Microsoft.DependencyInjection;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
@@ -29,7 +31,8 @@ namespace NzbDrone.Common.Test
.AddDummyDatabase()
.AddStartupContext(new StartupContext("first", "second"));
container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object);
container.RegisterInstance(new Mock<IHostLifetime>().Object);
container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object);
var serviceProvider = container.GetServiceProvider();

View File

@@ -30,7 +30,8 @@ namespace NzbDrone.Common.Disk
public abstract long? GetAvailableSpace(string path);
public abstract void InheritFolderPermissions(string filename);
public abstract void SetEveryonePermissions(string filename);
public abstract void SetPermissions(string path, string mask);
public abstract void SetFilePermissions(string path, string mask, string group);
public abstract void SetPermissions(string path, string mask, string group);
public abstract void CopyPermissions(string sourcePath, string targetPath);
public abstract long? GetTotalSize(string path);
@@ -130,7 +131,7 @@ namespace NzbDrone.Common.Disk
{
var testPath = Path.Combine(path, "prowlarr_write_test.txt");
var testContent = string.Format("This file was created to verify if '{0}' is writable. It should've been automatically deleted. Feel free to delete it.", path);
File.WriteAllText(testPath, testContent);
WriteAllText(testPath, testContent);
File.Delete(testPath);
return true;
}
@@ -258,17 +259,6 @@ namespace NzbDrone.Common.Disk
Ensure.That(source, () => source).IsValidPath();
Ensure.That(destination, () => destination).IsValidPath();
if (source.PathEquals(destination))
{
throw new IOException(string.Format("Source and destination can't be the same {0}", source));
}
if (FolderExists(destination) && overwrite)
{
DeleteFolder(destination, true);
}
RemoveReadOnlyFolder(source);
Directory.Move(source, destination);
}
@@ -310,7 +300,16 @@ namespace NzbDrone.Common.Disk
{
Ensure.That(filename, () => filename).IsValidPath();
RemoveReadOnly(filename);
File.WriteAllText(filename, contents);
// File.WriteAllText is broken on net core when writing to some CIFS mounts
// This workaround from https://github.com/dotnet/runtime/issues/42790#issuecomment-700362617
using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None))
{
using (var writer = new StreamWriter(fs))
{
writer.Write(contents);
}
}
}
public void FolderSetLastWriteTime(string path, DateTime dateTime)
@@ -550,7 +549,7 @@ namespace NzbDrone.Common.Disk
}
}
public virtual bool IsValidFilePermissionMask(string mask)
public virtual bool IsValidFolderPermissionMask(string mask)
{
throw new NotSupportedException();
}

View File

@@ -4,7 +4,6 @@ using System.Linq;
using System.Threading;
using NLog;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Disk
@@ -27,11 +26,56 @@ namespace NzbDrone.Common.Disk
_logger = logger;
}
private string ResolveRealParentPath(string path)
{
var parentPath = path.GetParentPath();
if (!_diskProvider.FolderExists(parentPath))
{
return path;
}
var realParentPath = parentPath.GetActualCasing();
var partialChildPath = path.Substring(parentPath.Length);
return realParentPath + partialChildPath;
}
public TransferMode TransferFolder(string sourcePath, string targetPath, TransferMode mode)
{
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
Ensure.That(targetPath, () => targetPath).IsValidPath();
sourcePath = ResolveRealParentPath(sourcePath);
targetPath = ResolveRealParentPath(targetPath);
_logger.Debug("{0} Directory [{1}] > [{2}]", mode, sourcePath, targetPath);
if (sourcePath == targetPath)
{
throw new IOException(string.Format("Source and destination can't be the same {0}", sourcePath));
}
if (mode == TransferMode.Move && sourcePath.PathEquals(targetPath, StringComparison.InvariantCultureIgnoreCase) && _diskProvider.FolderExists(targetPath))
{
// Move folder out of the way to allow case-insensitive renames
var tempPath = sourcePath + ".backup~";
_logger.Trace("Rename Intermediate Directory [{0}] > [{1}]", sourcePath, tempPath);
_diskProvider.MoveFolder(sourcePath, tempPath);
if (!_diskProvider.FolderExists(targetPath))
{
_logger.Trace("Rename Intermediate Directory [{0}] > [{1}]", tempPath, targetPath);
_logger.Debug("Rename Directory [{0}] > [{1}]", sourcePath, targetPath);
_diskProvider.MoveFolder(tempPath, targetPath);
return mode;
}
// There were two separate folders, revert the intermediate rename and let the recursion deal with it
_logger.Trace("Rename Intermediate Directory [{0}] > [{1}]", tempPath, sourcePath);
_diskProvider.MoveFolder(tempPath, sourcePath);
}
if (mode == TransferMode.Move && !_diskProvider.FolderExists(targetPath))
{
var sourceMount = _diskProvider.GetMount(sourcePath);
@@ -40,7 +84,7 @@ namespace NzbDrone.Common.Disk
// If we're on the same mount, do a simple folder move.
if (sourceMount != null && targetMount != null && sourceMount.RootDirectory == targetMount.RootDirectory)
{
_logger.Debug("Move Directory [{0}] > [{1}]", sourcePath, targetPath);
_logger.Debug("Rename Directory [{0}] > [{1}]", sourcePath, targetPath);
_diskProvider.MoveFolder(sourcePath, targetPath);
return mode;
}
@@ -79,6 +123,13 @@ namespace NzbDrone.Common.Disk
if (mode.HasFlag(TransferMode.Move))
{
var totalSize = _diskProvider.GetFileInfos(sourcePath).Sum(v => v.Length);
if (totalSize > (100 * 1024L * 1024L))
{
throw new IOException($"Large files still exist in {sourcePath} after folder move, not deleting source folder");
}
_diskProvider.DeleteFolder(sourcePath, true);
}
@@ -92,7 +143,10 @@ namespace NzbDrone.Common.Disk
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
Ensure.That(targetPath, () => targetPath).IsValidPath();
_logger.Debug("Mirror [{0}] > [{1}]", sourcePath, targetPath);
sourcePath = ResolveRealParentPath(sourcePath);
targetPath = ResolveRealParentPath(targetPath);
_logger.Debug("Mirror Folder [{0}] > [{1}]", sourcePath, targetPath);
if (!_diskProvider.FolderExists(targetPath))
{
@@ -204,6 +258,9 @@ namespace NzbDrone.Common.Disk
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
Ensure.That(targetPath, () => targetPath).IsValidPath();
sourcePath = ResolveRealParentPath(sourcePath);
targetPath = ResolveRealParentPath(targetPath);
_logger.Debug("{0} [{1}] > [{2}]", mode, sourcePath, targetPath);
var originalSize = _diskProvider.GetFileSize(sourcePath);

View File

@@ -11,7 +11,8 @@ namespace NzbDrone.Common.Disk
long? GetAvailableSpace(string path);
void InheritFolderPermissions(string filename);
void SetEveryonePermissions(string filename);
void SetPermissions(string path, string mask);
void SetFilePermissions(string path, string mask, string group);
void SetPermissions(string path, string mask, string group);
void CopyPermissions(string sourcePath, string targetPath);
long? GetTotalSize(string path);
DateTime FolderGetCreationTime(string path);
@@ -56,6 +57,6 @@ namespace NzbDrone.Common.Disk
List<FileInfo> GetFileInfos(string path, SearchOption searchOption = SearchOption.TopDirectoryOnly);
void RemoveEmptySubfolders(string path);
void SaveStream(Stream stream, string path);
bool IsValidFilePermissionMask(string mask);
bool IsValidFolderPermissionMask(string mask);
}
}

View File

@@ -1,15 +1,79 @@
using System;
using System.IO;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Common.Disk
{
public static class LongPathSupport
{
private static int MAX_PATH;
private static int MAX_NAME;
public static void Enable()
{
// Mono has an issue with enabling long path support via app.config.
// This works for both mono and .net on Windows.
AppContext.SetSwitch("Switch.System.IO.UseLegacyPathHandling", false);
AppContext.SetSwitch("Switch.System.IO.BlockLongPaths", false);
DetectLongPathLimits();
}
private static void DetectLongPathLimits()
{
if (!int.TryParse(Environment.GetEnvironmentVariable("MAX_PATH"), out MAX_PATH))
{
if (OsInfo.IsLinux)
{
MAX_PATH = 4096;
}
else
{
try
{
// Windows paths can be up to 32,767 characters long, but each component of the path must be less than 255.
// If the OS does not have Long Path enabled, then the following will throw an exception
// ref: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
Path.GetDirectoryName($@"C:\{new string('a', 254)}\{new string('a', 254)}");
MAX_PATH = 4096;
}
catch
{
MAX_PATH = 260 - 1;
}
}
}
if (!int.TryParse(Environment.GetEnvironmentVariable("MAX_NAME"), out MAX_NAME))
{
MAX_NAME = 255;
}
}
public static int MaxFilePathLength
{
get
{
if (MAX_PATH == 0)
{
DetectLongPathLimits();
}
return MAX_PATH;
}
}
public static int MaxFileNameLength
{
get
{
if (MAX_NAME == 0)
{
DetectLongPathLimits();
}
return MAX_NAME;
}
}
}
}

View File

@@ -11,7 +11,7 @@ namespace NzbDrone.Common.Instrumentation
private static readonly Regex[] CleansingRules = new[]
{
// Url
new Regex(@"(?<=[?&: ;])(apikey|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pwd)=(?<secret>[^&=]+?)(?= |&|$|<)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=[?&: ;])(apikey|(?:(?:access|api)[-_]?)?token|pass(?:key|wd)?|auth|authkey|user|u?id|api|[a-z_]*apikey|account|pwd)=(?<secret>[^&=""]+?)(?=[ ""&=]|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=[?& ;])[^=]*?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"rss\.torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"rss\.torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
@@ -28,6 +28,9 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"""/home/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
// NzbGet
new Regex(@"""Name""\s*:\s*""[^""]*(username|password)""\s*,\s*""Value""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),
@@ -51,7 +54,8 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"(?<=\?|&)(X-Plex-Client-Identifier|X-Plex-Token)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Indexer Responses
new Regex(@"(?:avistaz|exoticaz|cinemaz|privatehd)\.[a-z]{2,3}\\\/rss\\\/download\\\/(?<secret>[^&=]+?)\\\/(?<secret>[^&=]+?)\.torrent", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?:avistaz|exoticaz|cinemaz|privatehd)\.[a-z]{2,3}/rss/download/(?<secret>[^&=]+?)/(?<secret>[^&=]+?)\.torrent", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?:animebytes)\.[a-z]{2,3}/torrent/[0-9]+/download/(?<secret>[^&=]+?)[""]", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@",""info_hash"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@",""pass[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@",""rss[- _]?key"":""(?<secret>[^&=]+?)"",", RegexOptions.Compiled | RegexOptions.IgnoreCase),

View File

@@ -1,4 +1,6 @@
using System.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NLog.Fluent;
@@ -8,47 +10,46 @@ namespace NzbDrone.Common.Instrumentation.Extensions
{
public static readonly Logger SentryLogger = LogManager.GetLogger("Sentry");
public static LogBuilder SentryFingerprint(this LogBuilder logBuilder, params string[] fingerprint)
public static LogEventBuilder SentryFingerprint(this LogEventBuilder logBuilder, params string[] fingerprint)
{
return logBuilder.Property("Sentry", fingerprint);
}
public static LogBuilder WriteSentryDebug(this LogBuilder logBuilder, params string[] fingerprint)
public static LogEventBuilder WriteSentryDebug(this LogEventBuilder logBuilder, params string[] fingerprint)
{
return LogSentryMessage(logBuilder, LogLevel.Debug, fingerprint);
}
public static LogBuilder WriteSentryInfo(this LogBuilder logBuilder, params string[] fingerprint)
public static LogEventBuilder WriteSentryInfo(this LogEventBuilder logBuilder, params string[] fingerprint)
{
return LogSentryMessage(logBuilder, LogLevel.Info, fingerprint);
}
public static LogBuilder WriteSentryWarn(this LogBuilder logBuilder, params string[] fingerprint)
public static LogEventBuilder WriteSentryWarn(this LogEventBuilder logBuilder, params string[] fingerprint)
{
return LogSentryMessage(logBuilder, LogLevel.Warn, fingerprint);
}
public static LogBuilder WriteSentryError(this LogBuilder logBuilder, params string[] fingerprint)
public static LogEventBuilder WriteSentryError(this LogEventBuilder logBuilder, params string[] fingerprint)
{
return LogSentryMessage(logBuilder, LogLevel.Error, fingerprint);
}
private static LogBuilder LogSentryMessage(LogBuilder logBuilder, LogLevel level, string[] fingerprint)
private static LogEventBuilder LogSentryMessage(LogEventBuilder logBuilder, LogLevel level, string[] fingerprint)
{
SentryLogger.Log(level)
.CopyLogEvent(logBuilder.LogEventInfo)
SentryLogger.ForLogEvent(level)
.CopyLogEvent(logBuilder.LogEvent)
.SentryFingerprint(fingerprint)
.Write();
.Log();
return logBuilder.Property("Sentry", null);
return logBuilder.Property<string>("Sentry", null);
}
private static LogBuilder CopyLogEvent(this LogBuilder logBuilder, LogEventInfo logEvent)
private static LogEventBuilder CopyLogEvent(this LogEventBuilder logBuilder, LogEventInfo logEvent)
{
return logBuilder.LoggerName(logEvent.LoggerName)
.TimeStamp(logEvent.TimeStamp)
return logBuilder.TimeStamp(logEvent.TimeStamp)
.Message(logEvent.Message, logEvent.Parameters)
.Properties(logEvent.Properties.ToDictionary(v => v.Key, v => v.Value))
.Properties(logEvent.Properties.Select(p => new KeyValuePair<string, object>(p.Key.ToString(), p.Value)))
.Exception(logEvent.Exception);
}
}

View File

@@ -1,13 +1,16 @@
using NLog;
using System;
using System.Text;
using NLog;
using NLog.Targets;
namespace NzbDrone.Common.Instrumentation
{
public class NzbDroneFileTarget : FileTarget
{
protected override string GetFormattedMessage(LogEventInfo logEvent)
protected override void RenderFormattedMessage(LogEventInfo logEvent, StringBuilder target)
{
return CleanseLogMessage.Cleanse(Layout.Render(logEvent));
var result = CleanseLogMessage.Cleanse(Layout.Render(logEvent));
target.Append(result);
}
}
}

View File

@@ -34,6 +34,8 @@ namespace NzbDrone.Common.Instrumentation
var appFolderInfo = new AppFolderInfo(startupContext);
RegisterGlobalFilters();
if (Debugger.IsAttached)
{
RegisterDebugger();
@@ -97,10 +99,21 @@ namespace NzbDrone.Common.Instrumentation
target.Layout = "[${level}] [${threadid}] ${logger}: ${message} ${onexception:inner=${newline}${newline}[v${assembly-version}] ${exception:format=ToString}${newline}${exception:format=Data}${newline}}";
var loggingRule = new LoggingRule("*", LogLevel.Trace, target);
LogManager.Configuration.AddTarget("debugger", target);
LogManager.Configuration.LoggingRules.Add(loggingRule);
}
private static void RegisterGlobalFilters()
{
LogManager.Setup().LoadConfiguration(c =>
{
c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info);
c.ForLogger("System*").WriteToNil(LogLevel.Warn);
c.ForLogger("Microsoft*").WriteToNil(LogLevel.Warn);
});
}
private static void RegisterConsole()
{
var level = LogLevel.Trace;

View File

@@ -127,7 +127,18 @@ namespace NzbDrone.Common.Processes
try
{
_logger.Trace("Setting environment variable '{0}' to '{1}'", environmentVariable.Key, environmentVariable.Value);
startInfo.EnvironmentVariables.Add(environmentVariable.Key.ToString(), environmentVariable.Value.ToString());
var key = environmentVariable.Key.ToString();
var value = environmentVariable.Value?.ToString();
if (startInfo.EnvironmentVariables.ContainsKey(key))
{
startInfo.EnvironmentVariables[key] = value;
}
else
{
startInfo.EnvironmentVariables.Add(key, value);
}
}
catch (Exception e)
{

View File

@@ -9,8 +9,10 @@
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="4.7.14" />
<PackageReference Include="Sentry" Version="3.15.0" />
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="Sentry" Version="3.18.0" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />

View File

@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.Datastore
public void SingleOrDefault_should_return_null_on_empty_db()
{
Mocker.Resolve<IDatabase>()
.OpenConnection().Query<IndexerDefinition>("SELECT * FROM Indexers")
.OpenConnection().Query<IndexerDefinition>("SELECT * FROM \"Indexers\"")
.SingleOrDefault()
.Should()
.BeNull();

File diff suppressed because it is too large Load Diff

View File

@@ -5,10 +5,14 @@ using System.IO;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Npgsql;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Test.Common.Datastore;
namespace NzbDrone.Core.Test.Framework
{
@@ -49,6 +53,7 @@ namespace NzbDrone.Core.Test.Framework
public abstract class DbTest : CoreTest
{
private ITestDatabase _db;
private DatabaseType _databaseType;
protected virtual MigrationType MigrationType => MigrationType.Main;
@@ -101,17 +106,39 @@ namespace NzbDrone.Core.Test.Framework
private IDatabase CreateDatabase(MigrationContext migrationContext)
{
if (_databaseType == DatabaseType.PostgreSQL)
{
CreatePostgresDb();
}
var factory = Mocker.Resolve<DbFactory>();
// If a special migration test or log migration then create new
if (migrationContext.BeforeMigration != null)
if (migrationContext.BeforeMigration != null || _databaseType == DatabaseType.PostgreSQL)
{
return factory.Create(migrationContext);
}
return CreateSqliteDatabase(factory, migrationContext);
}
private void CreatePostgresDb()
{
var options = Mocker.Resolve<IOptions<PostgresOptions>>().Value;
PostgresDatabase.Create(options, MigrationType);
}
private void DropPostgresDb()
{
var options = Mocker.Resolve<IOptions<PostgresOptions>>().Value;
PostgresDatabase.Drop(options, MigrationType);
}
private IDatabase CreateSqliteDatabase(IDbFactory factory, MigrationContext migrationContext)
{
// Otherwise try to use a cached migrated db
var cachedDb = GetCachedDatabase(migrationContext.MigrationType);
var testDb = GetTestDb(migrationContext.MigrationType);
var cachedDb = SqliteDatabase.GetCachedDb(migrationContext.MigrationType);
var testDb = GetTestSqliteDb(migrationContext.MigrationType);
if (File.Exists(cachedDb))
{
TestLogger.Info($"Using cached initial database {cachedDb}");
@@ -131,12 +158,7 @@ namespace NzbDrone.Core.Test.Framework
}
}
private string GetCachedDatabase(MigrationType type)
{
return Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_{type}.db");
}
private string GetTestDb(MigrationType type)
private string GetTestSqliteDb(MigrationType type)
{
return type == MigrationType.Main ? TestFolderInfo.GetDatabase() : TestFolderInfo.GetLogDatabase();
}
@@ -151,6 +173,13 @@ namespace NzbDrone.Core.Test.Framework
WithTempAsAppPath();
SetupLogging();
// populate the possible postgres options
var postgresOptions = PostgresDatabase.GetTestOptions();
_databaseType = postgresOptions.Host.IsNotNullOrWhiteSpace() ? DatabaseType.PostgreSQL : DatabaseType.SQLite;
// Set up remaining container services
Mocker.SetConstant(Options.Create(postgresOptions));
Mocker.SetConstant<IConfigFileProvider>(Mocker.Resolve<ConfigFileProvider>());
Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>());
Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>());
@@ -171,11 +200,17 @@ namespace NzbDrone.Core.Test.Framework
GC.Collect();
GC.WaitForPendingFinalizers();
SQLiteConnection.ClearAllPools();
NpgsqlConnection.ClearAllPools();
if (TestFolderInfo != null)
{
DeleteTempFolder(TestFolderInfo.AppDataFolder);
}
if (_databaseType == DatabaseType.PostgreSQL)
{
DropPostgresDb();
}
}
}
}

View File

@@ -1,5 +1,7 @@
using System.IO;
using NUnit.Framework;
using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Test.Common.Datastore;
namespace NzbDrone.Core.Test
{
@@ -10,13 +12,13 @@ namespace NzbDrone.Core.Test
[OneTimeTearDown]
public void ClearCachedDatabase()
{
var mainCache = Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_Main.db");
var mainCache = SqliteDatabase.GetCachedDb(MigrationType.Main);
if (File.Exists(mainCache))
{
File.Delete(mainCache);
}
var logCache = Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_Log.db");
var logCache = SqliteDatabase.GetCachedDb(MigrationType.Log);
if (File.Exists(logCache))
{
File.Delete(logCache);

View File

@@ -23,6 +23,7 @@ namespace NzbDrone.Core.Test.Framework
where T : ModelBase, new();
IDirectDataMapper GetDirectDataMapper();
IDbConnection OpenConnection();
DatabaseType DatabaseType { get; }
}
public class TestDatabase : ITestDatabase
@@ -30,6 +31,8 @@ namespace NzbDrone.Core.Test.Framework
private readonly IDatabase _dbConnection;
private readonly IEventAggregator _eventAggregator;
public DatabaseType DatabaseType => _dbConnection.DatabaseType;
public TestDatabase(IDatabase dbConnection)
{
_eventAggregator = new Mock<IEventAggregator>().Object;

View File

@@ -41,7 +41,8 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
Subject.Clean();
AllStoredModels.ToList().ForEach(t => t.LastExecution.Should().Be(expectedTime));
// BeCloseTo handles Postgres rounding times
AllStoredModels.ToList().ForEach(t => t.LastExecution.Should().BeCloseTo(expectedTime));
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Definitions;
using NzbDrone.Core.Indexers.Definitions.Avistaz;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
{
[TestFixture]
public class ExoticazFixture : CoreTest<ExoticaZ>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
{
Name = "ExoticaZ",
Settings = new AvistazSettings() { Username = "someuser", Password = "somepass", Pid = "somepid" }
};
}
[Test]
public async Task should_parse_recent_feed_from_ExoticaZ()
{
var recentFeed = ReadAllText(@"Files/Indexers/Exoticaz/recentfeed.json");
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
releases.Should().HaveCount(100);
releases.First().Should().BeOfType<TorrentInfo>();
var torrentInfo = releases.First() as TorrentInfo;
torrentInfo.Title.Should().Be("[SSIS-419] My first experience is Yua Mikami. From the day I lost my virginity, I was devoted to sex.");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://exoticaz.to/rss/download/(removed)/(removed).torrent");
torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 11:04:50"));
torrentInfo.Size.Should().Be(7085405541);
torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf");
torrentInfo.MagnetUrl.Should().Be(null);
torrentInfo.Peers.Should().Be(33);
torrentInfo.Seeders.Should().Be(33);
torrentInfo.Categories.First().Id.Should().Be(6040);
}
}
}

View File

@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Indexers.Cardigann;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.CardigannTests
{
public class ApplyGoTemplateTextFixture : CoreTest<CardigannBase>
{
private Dictionary<string, object> _variables;
private CardigannDefinition _definition;
[SetUp]
public void SetUp()
{
_variables = new Dictionary<string, object>
{
[".Config.sitelink"] = "https://somesite.com/",
[".True"] = "True",
[".False"] = null,
[".Today.Year"] = DateTime.Today.Year.ToString(),
[".Categories"] = new string[] { "tv", "movies" }
};
_definition = Builder<CardigannDefinition>.CreateNew()
.With(x => x.Encoding = "UTF-8")
.With(x => x.Links = new List<string>
{
"https://somesite.com/"
})
.With(x => x.Caps = new CapabilitiesBlock
{
Modes = new Dictionary<string, List<string>>
{
{ "search", new List<string> { "q" } }
}
})
.Build();
Mocker.SetConstant<CardigannDefinition>(_definition);
}
[TestCase("{{ range .Categories}}&categories[]={{.}}{{end}}", "&categories[]=tv&categories[]=movies")]
[TestCase("{{ range $i, $e := .Categories}}&categories[{{$i}}]={{.}}{{end}}", "&categories[0]=tv&categories[1]=movies")]
[TestCase("{{ range $index, $element := .Categories}}&categories[{{$index}}]={{.}}+postIndex[{{$index}}]{{end}}", "&categories[0]=tv+postIndex[0]&categories[1]=movies+postIndex[1]")]
public void should_handle_range_statements(string template, string expected)
{
var result = Subject.ApplyGoTemplateText(template, _variables);
result.Should().Be(expected);
}
[TestCase("{{ re_replace .Query.Keywords \"[^a-zA-Z0-9]+\" \"%\" }}", "abc%def")]
public void should_handle_re_replace_statements(string template, string expected)
{
_variables[".Query.Keywords"] = string.Join(" ", new List<string> { "abc", "def" });
var result = Subject.ApplyGoTemplateText(template, _variables);
result.Should().Be(expected);
}
[TestCase("{{ join .Categories \", \" }}", "tv, movies")]
public void should_handle_join_statements(string template, string expected)
{
var result = Subject.ApplyGoTemplateText(template, _variables);
result.Should().Be(expected);
}
[TestCase("{{ .Today.Year }}", "2022")]
public void should_handle_variables_statements(string template, string expected)
{
var result = Subject.ApplyGoTemplateText(template, _variables);
result.Should().Be(expected);
}
[TestCase("{{if .False }}0{{else}}1{{end}}", "1")]
[TestCase("{{if .True }}0{{else}}1{{end}}", "0")]
public void should_handle_if_statements(string template, string expected)
{
var result = Subject.ApplyGoTemplateText(template, _variables);
result.Should().Be(expected);
}
}
}

View File

@@ -48,7 +48,7 @@ namespace NzbDrone.Core.Test.IndexerTests.PTPTests
var torrents = (await Subject.Fetch(new MovieSearchCriteria())).Releases;
torrents.Should().HaveCount(293);
torrents.First().Should().BeOfType<PassThePopcornInfo>();
torrents.First().Should().BeOfType<TorrentInfo>();
var first = torrents.First() as TorrentInfo;

View File

@@ -0,0 +1,36 @@
using System;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Security;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.SecurityTests
{
[TestFixture]
public class ProtectionServiceFixture : CoreTest<ProtectionService>
{
private string _protectionKey;
[SetUp]
public void Setup()
{
_protectionKey = Guid.NewGuid().ToString().Replace("-", "");
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.DownloadProtectionKey)
.Returns(_protectionKey);
}
[Test]
public void should_encrypt_and_decrypt_string()
{
const string plainText = "https://prowlarr.com";
var encrypted = Subject.Protect(plainText);
var decrypted = Subject.UnProtect(encrypted);
decrypted.Should().Be(plainText);
}
}
}

View File

@@ -68,6 +68,10 @@ namespace NzbDrone.Core.Test.UpdateTests
.Setup(c => c.FolderWritable(It.IsAny<string>()))
.Returns(true);
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Prowlarr.Update.exe"))))
.Returns(true);
_sandboxFolder = Mocker.GetMock<IAppFolderInfo>().Object.GetUpdateSandboxFolder();
}
@@ -149,7 +153,7 @@ namespace NzbDrone.Core.Test.UpdateTests
}
[Test]
public void should_start_update_client()
public void should_start_update_client_if_updater_exists()
{
Subject.Execute(new ApplicationUpdateCommand());
@@ -157,6 +161,21 @@ namespace NzbDrone.Core.Test.UpdateTests
.Verify(c => c.Start(It.IsAny<string>(), It.Is<string>(s => s.StartsWith("12")), null, null, null), Times.Once());
}
[Test]
public void should_return_with_warning_if_updater_doesnt_exists()
{
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.Is<string>(s => s.EndsWith("Prowlarr.Update.exe"))))
.Returns(false);
Subject.Execute(new ApplicationUpdateCommand());
Mocker.GetMock<IProcessProvider>()
.Verify(c => c.Start(It.IsAny<string>(), It.IsAny<string>(), null, null, null), Times.Never());
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_return_without_error_or_warnings_when_no_updates_are_available()
{

View File

@@ -28,10 +28,13 @@ namespace NzbDrone.Core.Applications.Lidarr
}
var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value;
var apiPath = (string)Fields.FirstOrDefault(x => x.Name == "apiPath").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value;
var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value);
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var minimumSeedersCompare = minimumSeeders == otherMinimumSeeders;
@@ -40,8 +43,8 @@ namespace NzbDrone.Core.Applications.Lidarr
var otherSeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value);
var seedTimeCompare = seedTime == otherSeedTime;
var discographySeedTime = Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value);
var otherDiscographySeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value);
var discographySeedTime = Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value);
var otherDiscographySeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value);
var discographySeedTimeCompare = discographySeedTime == otherDiscographySeedTime;
var seedRatio = Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value);
@@ -55,7 +58,7 @@ namespace NzbDrone.Core.Applications.Lidarr
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPath && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare;
apiKey && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare;
}
}
}

View File

@@ -28,10 +28,13 @@ namespace NzbDrone.Core.Applications.Radarr
}
var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value;
var apiPath = (string)Fields.FirstOrDefault(x => x.Name == "apiPath").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value;
var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value);
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var minimumSeedersCompare = minimumSeeders == otherMinimumSeeders;
@@ -51,7 +54,7 @@ namespace NzbDrone.Core.Applications.Radarr
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPath && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare;
apiKey && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare;
}
}
}

View File

@@ -28,10 +28,13 @@ namespace NzbDrone.Core.Applications.Readarr
}
var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value;
var apiPath = (string)Fields.FirstOrDefault(x => x.Name == "apiPath").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value;
var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value);
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var minimumSeedersCompare = minimumSeeders == otherMinimumSeeders;
@@ -40,8 +43,8 @@ namespace NzbDrone.Core.Applications.Readarr
var otherSeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value);
var seedTimeCompare = seedTime == otherSeedTime;
var discographySeedTime = Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value);
var otherDiscographySeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seasonPackSeedTime").Value);
var discographySeedTime = Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value);
var otherDiscographySeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.discographySeedTime").Value);
var discographySeedTimeCompare = discographySeedTime == otherDiscographySeedTime;
var seedRatio = Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value);
@@ -55,7 +58,7 @@ namespace NzbDrone.Core.Applications.Readarr
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPath && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare;
apiKey && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && discographySeedTimeCompare;
}
}
}

View File

@@ -28,11 +28,14 @@ namespace NzbDrone.Core.Applications.Sonarr
}
var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value;
var apiPath = (string)Fields.FirstOrDefault(x => x.Name == "apiPath").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value;
var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value);
var animeCats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "animeCategories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "animeCategories").Value);
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var minimumSeedersCompare = minimumSeeders == otherMinimumSeeders;
@@ -56,7 +59,7 @@ namespace NzbDrone.Core.Applications.Sonarr
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPath && baseUrl && cats && animeCats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare;
apiKey && apiPathCompare && baseUrl && cats && animeCats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare && seasonSeedTimeCompare;
}
}
}

View File

@@ -28,10 +28,13 @@ namespace NzbDrone.Core.Applications.Whisparr
}
var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value;
var apiPath = (string)Fields.FirstOrDefault(x => x.Name == "apiPath").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value;
var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value);
var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiPathCompare = apiPath == otherApiPath;
var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value);
var minimumSeedersCompare = minimumSeeders == otherMinimumSeeders;
@@ -51,7 +54,7 @@ namespace NzbDrone.Core.Applications.Whisparr
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPath && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare;
apiKey && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare;
}
}
}

View File

@@ -5,12 +5,14 @@ using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Extensions.Options;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
@@ -67,6 +69,7 @@ namespace NzbDrone.Core.Configuration
private readonly IEventAggregator _eventAggregator;
private readonly IDiskProvider _diskProvider;
private readonly ICached<string> _cache;
private readonly PostgresOptions _postgresOptions;
private readonly string _configFile;
private static readonly Regex HiddenCharacterRegex = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
@@ -76,12 +79,14 @@ namespace NzbDrone.Core.Configuration
public ConfigFileProvider(IAppFolderInfo appFolderInfo,
ICacheManager cacheManager,
IEventAggregator eventAggregator,
IDiskProvider diskProvider)
IDiskProvider diskProvider,
IOptions<PostgresOptions> postgresOptions)
{
_cache = cacheManager.GetCache<string>(GetType());
_eventAggregator = eventAggregator;
_diskProvider = diskProvider;
_configFile = appFolderInfo.GetConfigPath();
_postgresOptions = postgresOptions.Value;
}
public Dictionary<string, object> GetConfigDictionary()
@@ -195,13 +200,13 @@ namespace NzbDrone.Core.Configuration
public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant();
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
public string PostgresHost => GetValue("PostgresHost", string.Empty, persist: false);
public string PostgresUser => GetValue("PostgresUser", string.Empty, persist: false);
public string PostgresPassword => GetValue("PostgresPassword", string.Empty, persist: false);
public string PostgresMainDb => GetValue("PostgresMainDb", "prowlarr-main", persist: false);
public string PostgresLogDb => GetValue("PostgresLogDb", "prowlarr-log", persist: false);
public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false);
public string PostgresUser => _postgresOptions?.User ?? GetValue("PostgresUser", string.Empty, persist: false);
public string PostgresPassword => _postgresOptions?.Password ?? GetValue("PostgresPassword", string.Empty, persist: false);
public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "prowlarr-main", persist: false);
public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "prowlarr-log", persist: false);
public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false);
public string Theme => GetValue("Theme", "light", persist: false);
public int PostgresPort => GetValueInt("PostgresPort", 5432, persist: false);
public bool LogSql => GetValueBoolean("LogSql", false, persist: false);
public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);

View File

@@ -167,14 +167,12 @@ namespace NzbDrone.Core.Datastore
}
}
if (_database.DatabaseType == DatabaseType.SQLite)
{
return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id";
}
else
if (_database.DatabaseType == DatabaseType.PostgreSQL)
{
return $"INSERT INTO \"{_table}\" ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}) RETURNING \"Id\"";
}
return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id";
}
private TModel Insert(IDbConnection connection, IDbTransaction transaction, TModel model)

View File

@@ -24,7 +24,6 @@ namespace NzbDrone.Core.Datastore.Migration
Delete.FromTable("Indexers").Row(new { Implementation = "ThePirateBay" });
Delete.FromTable("Indexers").Row(new { Implementation = "TorrentLeech" });
Delete.FromTable("Indexers").Row(new { Implementation = "TorrentSeeds" });
Delete.FromTable("Indexers").Row(new { Implementation = "TorrentParadiseMI" });
Delete.FromTable("Indexers").Row(new { Implementation = "YTS" });
//Change settings to shared classes

View File

@@ -3,7 +3,7 @@ using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(18)]
[Migration(018)]
public class minimum_seeders : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()

View File

@@ -0,0 +1,15 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(019)]
public class remove_showrss : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
// Remove, YML version exists
Delete.FromTable("Indexers").Row(new { Implementation = "ShowRSS" });
}
}
}

View File

@@ -0,0 +1,15 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(020)]
public class remove_torrentparadiseml : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
// Remove, 017 incorrectly removes this using "TorrentParadiseMI"
Delete.FromTable("Indexers").Row(new { Implementation = "TorrentParadiseMl" });
}
}
}

View File

@@ -0,0 +1,26 @@
using Microsoft.Extensions.Configuration;
namespace NzbDrone.Core.Datastore
{
public class PostgresOptions
{
public string Host { get; set; }
public int Port { get; set; }
public string User { get; set; }
public string Password { get; set; }
public string MainDb { get; set; }
public string LogDb { get; set; }
public static PostgresOptions GetOptions()
{
var config = new ConfigurationBuilder()
.AddEnvironmentVariables()
.Build();
var postgresOptions = new PostgresOptions();
config.GetSection("Prowlarr:Postgres").Bind(postgresOptions);
return postgresOptions;
}
}
}

View File

@@ -313,7 +313,20 @@ namespace NzbDrone.Core.Datastore
_sb.Append(" = ANY (");
Visit(list);
// hardcode the integer list if it exists to bypass parameter limit
if (item.Type == typeof(int) && TryGetRightValue(list, out var value))
{
var items = (IEnumerable<int>)value;
_sb.Append("('{");
_sb.Append(string.Join(", ", items));
_sb.Append("}')");
_gotConcreteValue = true;
}
else
{
Visit(list);
}
_sb.Append("))");
}
@@ -324,7 +337,7 @@ namespace NzbDrone.Core.Datastore
Visit(body.Object);
_sb.Append(" LIKE '%' || ");
_sb.Append(" ILIKE '%' || ");
Visit(body.Arguments[0]);
@@ -337,7 +350,7 @@ namespace NzbDrone.Core.Datastore
Visit(body.Object);
_sb.Append(" LIKE ");
_sb.Append(" ILIKE ");
Visit(body.Arguments[0]);
@@ -350,7 +363,7 @@ namespace NzbDrone.Core.Datastore
Visit(body.Object);
_sb.Append(" LIKE '%' || ");
_sb.Append(" ILIKE '%' || ");
Visit(body.Arguments[0]);

View File

@@ -114,6 +114,7 @@ namespace NzbDrone.Core.Download
public async Task<byte[]> DownloadReport(string link, int indexerId, string source, string host, string title)
{
_logger.Trace("Attempting download of {0}", link);
var url = new Uri(link);
// Limit grabs to 2 per second.

View File

@@ -23,11 +23,10 @@ namespace NzbDrone.Core.HealthCheck.Checks
public override HealthCheck Check()
{
var enabled = _indexerFactory.Enabled(false);
var indexers = _indexerFactory.AllProviders(false);
var expiringProviders = new List<IIndexer>();
var expiredProviders = new List<IIndexer>();
foreach (var provider in enabled)
foreach (var provider in indexers)
{
var settingsType = provider.Definition.Settings.GetType();
var vipProp = settingsType.GetProperty("VipExpiration");
@@ -44,11 +43,6 @@ namespace NzbDrone.Core.HealthCheck.Checks
continue;
}
if (DateTime.Parse(expiration).Before(DateTime.Now))
{
expiredProviders.Add(provider);
}
if (DateTime.Parse(expiration).Between(DateTime.Now, DateTime.Now.AddDays(7)))
{
expiringProviders.Add(provider);
@@ -64,15 +58,6 @@ namespace NzbDrone.Core.HealthCheck.Checks
"#indexer-vip-expiring");
}
if (!expiredProviders.Empty())
{
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("IndexerVipCheckExpiredClientMessage"),
string.Join(", ", expiredProviders.Select(v => v.Definition.Name))),
"#indexer-vip-expired");
}
return new HealthCheck(GetType());
}
}

View File

@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Localization;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderAddedEvent<IIndexer>))]
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
public class IndexerVIPExpiredCheck : HealthCheckBase
{
private readonly IIndexerFactory _indexerFactory;
public IndexerVIPExpiredCheck(IIndexerFactory indexerFactory, ILocalizationService localizationService)
: base(localizationService)
{
_indexerFactory = indexerFactory;
}
public override HealthCheck Check()
{
var indexers = _indexerFactory.AllProviders(false);
var expiredProviders = new List<IIndexer>();
foreach (var provider in indexers)
{
var settingsType = provider.Definition.Settings.GetType();
var vipProp = settingsType.GetProperty("VipExpiration");
if (vipProp == null)
{
continue;
}
var expiration = (string)vipProp.GetValue(provider.Definition.Settings);
if (expiration.IsNullOrWhiteSpace())
{
continue;
}
if (DateTime.Parse(expiration).Before(DateTime.Now))
{
expiredProviders.Add(provider);
}
}
if (!expiredProviders.Empty())
{
return new HealthCheck(GetType(),
HealthCheckResult.Error,
string.Format(_localizationService.GetLocalizedString("IndexerVipCheckExpiredClientMessage"),
string.Join(", ", expiredProviders.Select(v => v.Definition.Name))),
"#indexer-vip-expired");
}
return new HealthCheck(GetType());
}
}
}

View File

@@ -77,12 +77,15 @@ namespace NzbDrone.Core.HealthCheck
.ToDictionary(g => g.Key, g => g.ToArray());
}
private void PerformHealthCheck(IProvideHealthCheck[] healthChecks)
private void PerformHealthCheck(IProvideHealthCheck[] healthChecks, bool performServerChecks = false)
{
var results = healthChecks.Select(c => c.Check())
.ToList();
results.AddRange(_serverSideNotificationService.GetServerChecks());
if (performServerChecks)
{
results.AddRange(_serverSideNotificationService.GetServerChecks());
}
foreach (var result in results)
{
@@ -108,17 +111,17 @@ namespace NzbDrone.Core.HealthCheck
{
if (message.Trigger == CommandTrigger.Manual)
{
PerformHealthCheck(_healthChecks);
PerformHealthCheck(_healthChecks, true);
}
else
{
PerformHealthCheck(_scheduledHealthChecks);
PerformHealthCheck(_scheduledHealthChecks, true);
}
}
public void HandleAsync(ApplicationStartedEvent message)
{
PerformHealthCheck(_startupHealthChecks);
PerformHealthCheck(_startupHealthChecks, true);
}
public void HandleAsync(IEvent message)

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http;
@@ -23,24 +24,37 @@ namespace NzbDrone.Core.HealthCheck
private readonly IHttpRequestBuilderFactory _cloudRequestBuilder;
private readonly Logger _logger;
public ServerSideNotificationService(IHttpClient client, IConfigFileProvider configFileProvider, IProwlarrCloudRequestBuilder cloudRequestBuilder, Logger logger)
private readonly ICached<List<HealthCheck>> _cache;
public ServerSideNotificationService(IHttpClient client,
IConfigFileProvider configFileProvider,
IProwlarrCloudRequestBuilder cloudRequestBuilder,
ICacheManager cacheManager,
Logger logger)
{
_client = client;
_configFileProvider = configFileProvider;
_cloudRequestBuilder = cloudRequestBuilder.Services;
_logger = logger;
_cache = cacheManager.GetCache<List<HealthCheck>>(GetType());
}
public List<HealthCheck> GetServerChecks()
{
return _cache.Get("ServerChecks", () => RetrieveServerChecks(), TimeSpan.FromHours(2));
}
private List<HealthCheck> RetrieveServerChecks()
{
var request = _cloudRequestBuilder.Create()
.Resource("/notification")
.AddQueryParam("version", BuildInfo.Version)
.AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant())
.AddQueryParam("arch", RuntimeInformation.OSArchitecture)
.AddQueryParam("runtime", PlatformInfo.Platform.ToString().ToLowerInvariant())
.AddQueryParam("branch", _configFileProvider.Branch)
.Build();
.Resource("/notification")
.AddQueryParam("version", BuildInfo.Version)
.AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant())
.AddQueryParam("arch", RuntimeInformation.OSArchitecture)
.AddQueryParam("runtime", PlatformInfo.Platform.ToString().ToLowerInvariant())
.AddQueryParam("branch", _configFileProvider.Branch)
.Build();
try
{
_logger.Trace("Getting server side health notifications");

View File

@@ -136,6 +136,7 @@ namespace NzbDrone.Core.History
{
history.Data.Add("ImdbId", ((TvSearchCriteria)message.Query).FullImdbId ?? string.Empty);
history.Data.Add("TvdbId", ((TvSearchCriteria)message.Query).TvdbId?.ToString() ?? string.Empty);
history.Data.Add("TmdbId", ((TvSearchCriteria)message.Query).TmdbId?.ToString() ?? string.Empty);
history.Data.Add("TraktId", ((TvSearchCriteria)message.Query).TraktId?.ToString() ?? string.Empty);
history.Data.Add("RId", ((TvSearchCriteria)message.Query).RId?.ToString() ?? string.Empty);
history.Data.Add("TvMazeId", ((TvSearchCriteria)message.Query).TvMazeId?.ToString() ?? string.Empty);

View File

@@ -75,6 +75,7 @@ namespace NzbDrone.Core.IndexerSearch
let t = (r as TorrentInfo) ?? new TorrentInfo()
select new XElement("item",
new XElement("title", RemoveInvalidXMLChars(r.Title)),
new XElement("description", RemoveInvalidXMLChars(r.Description)),
new XElement("guid", r.Guid), // GUID and (Link or Magnet) are mandatory
new XElement("prowlarrindexer", new XAttribute("id", r.IndexerId), r.Indexer),
r.InfoUrl == null ? null : new XElement("comments", r.InfoUrl),
@@ -97,6 +98,7 @@ namespace NzbDrone.Core.IndexerSearch
GetNabElement("imdb", r.ImdbId.ToString("D7"), protocol),
GetNabElement("tmdbid", r.TmdbId, protocol),
GetNabElement("traktid", r.TraktId, protocol),
GetNabElement("doubanid", r.DoubanId, protocol),
GetNabElement("seeders", t.Seeders, protocol),
GetNabElement("files", r.Files, protocol),
GetNabElement("grabs", r.Grabs, protocol),

View File

@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using NLog;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
@@ -14,17 +16,19 @@ namespace NzbDrone.Core.IndexerSearch
private readonly IHttpClient _httpClient;
private readonly IHttpRequestBuilderFactory _requestBuilder;
private readonly IAnalyticsService _analyticsService;
private readonly Logger _logger;
public ReleaseAnalyticsService(IHttpClient httpClient, IProwlarrCloudRequestBuilder requestBuilder, IAnalyticsService analyticsService)
public ReleaseAnalyticsService(IHttpClient httpClient, IProwlarrCloudRequestBuilder requestBuilder, IAnalyticsService analyticsService, Logger logger)
{
_analyticsService = analyticsService;
_requestBuilder = requestBuilder.Releases;
_httpClient = httpClient;
_logger = logger;
}
public void HandleAsync(IndexerQueryEvent message)
{
if (_analyticsService.IsEnabled)
if (_analyticsService.IsEnabled && message.QueryResult?.Releases != null)
{
var request = _requestBuilder.Create().Resource("release/push").Build();
request.Method = HttpMethod.Post;
@@ -34,14 +38,21 @@ namespace NzbDrone.Core.IndexerSearch
var body = message.QueryResult.Releases.Select(x => new
{
Title = x.Title,
Categories = x.Categories.Where(c => c.Id < 10000).Select(c => c.Id),
Categories = x.Categories?.Where(c => c.Id < 10000).Select(c => c.Id) ?? new List<int>(),
Protocol = x.DownloadProtocol.ToString(),
Size = x.Size,
PublishDate = x.PublishDate
});
request.SetContent(body.ToJson());
_httpClient.Post(request);
try
{
request.SetContent(body.ToJson());
_httpClient.Post(request);
}
catch
{
_logger.Trace("Analytics push failed");
}
}
}
}

View File

@@ -28,7 +28,7 @@ namespace NzbDrone.Core.IndexerVersions
/* Update Service will fall back if version # does not exist for an indexer per Ta */
private const string DEFINITION_BRANCH = "master";
private const int DEFINITION_VERSION = 5;
private const int DEFINITION_VERSION = 6;
//Used when moving yml to C#
private readonly List<string> _defintionBlocklist = new List<string>()

View File

@@ -6,11 +6,9 @@ using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using AngleSharp.Html.Parser;
using FluentValidation;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Settings;
@@ -18,8 +16,6 @@ using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
{

View File

@@ -8,6 +8,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
{
public string Url { get; set; }
public string Download { get; set; }
public Dictionary<string, string> Category { get; set; }
[JsonProperty(PropertyName = "movie_tv")]
public AvistazIdInfo MovieTvinfo { get; set; }

View File

@@ -201,6 +201,11 @@ namespace NzbDrone.Core.Indexers.Definitions
var jsonResponse = new HttpResponse<BeyondHDResponse>(indexerHttpResponse);
if (jsonResponse.Resource.StatusCode == 0)
{
throw new IndexerException(indexerResponse, $"Indexer Error: {jsonResponse.Resource.StatusMessage}");
}
foreach (var row in jsonResponse.Resource.Results)
{
var details = row.InfoUrl;
@@ -272,6 +277,11 @@ namespace NzbDrone.Core.Indexers.Definitions
public class BeyondHDResponse
{
[JsonProperty(PropertyName = "status_code")]
public int StatusCode { get; set; }
[JsonProperty(PropertyName = "status_message")]
public string StatusMessage { get; set; }
public List<BeyondHDTorrent> Results { get; set; }
}

View File

@@ -25,10 +25,10 @@ namespace NzbDrone.Core.Indexers.Cardigann
protected virtual string SiteLink { get; private set; }
protected readonly List<CategoryMapping> _categoryMapping = new List<CategoryMapping>();
protected readonly IndexerCapabilitiesCategories _categories = new IndexerCapabilitiesCategories();
protected readonly List<string> _defaultCategories = new List<string>();
protected readonly string[] OptionalFields = new string[] { "imdb", "imdbid", "rageid", "tmdbid", "tvdbid", "poster", "banner", "description" };
protected readonly string[] OptionalFields = new string[] { "imdb", "imdbid", "rageid", "tmdbid", "tvdbid", "poster", "banner", "description", "doubanid" };
protected static readonly string[] _SupportedLogicFunctions =
{
@@ -75,7 +75,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
continue;
}
AddCategoryMapping(category.Key, cat);
_categories.AddCategoryMapping(category.Key, cat);
}
}
@@ -95,7 +95,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
}
}
AddCategoryMapping(categorymapping.id, torznabCat, categorymapping.desc);
_categories.AddCategoryMapping(categorymapping.id, torznabCat, categorymapping.desc);
if (categorymapping.Default)
{
@@ -105,30 +105,6 @@ namespace NzbDrone.Core.Indexers.Cardigann
}
}
public void AddCategoryMapping(string trackerCategory, IndexerCategory torznabCategory, string trackerCategoryDesc = null)
{
_categoryMapping.Add(new CategoryMapping(trackerCategory, trackerCategoryDesc, torznabCategory.Id));
if (trackerCategoryDesc == null)
{
return;
}
// create custom cats (1:1 categories) if trackerCategoryDesc is defined
// - if trackerCategory is "integer" we use that number to generate custom category id
// - if trackerCategory is "string" we compute a hash to generate fixed integer id for the custom category
// the hash is not perfect but it should work in most cases. we can't use sequential numbers because
// categories are updated frequently and the id must be fixed to work in 3rd party apps
if (!int.TryParse(trackerCategory, out var trackerCategoryInt))
{
var hashed = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(trackerCategory));
trackerCategoryInt = BitConverter.ToUInt16(hashed, 0); // id between 0 and 65535 < 100000
}
var customCat = new IndexerCategory(trackerCategoryInt + 100000, trackerCategoryDesc);
_categoryMapping.Add(new CategoryMapping(trackerCategory, trackerCategoryDesc, customCat.Id));
}
protected IElement QuerySelector(IElement element, string selector)
{
// AngleSharp doesn't support the :root pseudo selector, so we check for it manually
@@ -362,57 +338,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
return variables;
}
protected ICollection<IndexerCategory> MapTrackerCatToNewznab(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
return new List<IndexerCategory>();
}
public delegate string TemplateTextModifier(string str);
var cats = _categoryMapping
.Where(m =>
!string.IsNullOrWhiteSpace(m.TrackerCategory) &&
string.Equals(m.TrackerCategory, input, StringComparison.InvariantCultureIgnoreCase))
.Select(c => NewznabStandardCategory.AllCats.FirstOrDefault(n => n.Id == c.NewzNabCategory) ?? new IndexerCategory { Id = c.NewzNabCategory })
.ToList();
return cats;
}
public List<string> MapTorznabCapsToTrackers(int[] searchCategories, bool mapChildrenCatsToParent = false)
{
if (searchCategories == null)
{
return new List<string>();
}
var results = new List<string>();
results.AddRange(_categoryMapping
.Where(c => searchCategories.Contains(c.NewzNabCategory))
.Select(mapping => mapping.TrackerCategory).Distinct().ToList());
return results;
}
public ICollection<IndexerCategory> MapTrackerCatDescToNewznab(string trackerCategoryDesc)
{
if (string.IsNullOrWhiteSpace(trackerCategoryDesc))
{
return new List<IndexerCategory>();
}
var cats = _categoryMapping
.Where(m =>
!string.IsNullOrWhiteSpace(m.TrackerCategoryDesc) &&
string.Equals(m.TrackerCategoryDesc, trackerCategoryDesc, StringComparison.InvariantCultureIgnoreCase))
.Select(c => NewznabStandardCategory.AllCats.FirstOrDefault(n => n.Id == c.NewzNabCategory) ?? new IndexerCategory { Id = c.NewzNabCategory })
.ToList();
return cats;
}
protected delegate string TemplateTextModifier(string str);
protected string ApplyGoTemplateText(string template, Dictionary<string, object> variables = null, TemplateTextModifier modifier = null)
public string ApplyGoTemplateText(string template, Dictionary<string, object> variables = null, TemplateTextModifier modifier = null)
{
if (variables == null)
{
@@ -592,7 +520,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
}
// handle range expression
var rangeRegex = new Regex(@"{{\s*range\s*(.+?)\s*}}(.*?){{\.}}(.*?){{end}}");
var rangeRegex = new Regex(@"{{\s*range\s*(((?<index>\$.+?),)((\s*(?<element>.+?)\s*(:=)\s*)))?(?<variable>.+?)\s*}}(?<prefix>.*?){{\.}}(?<postfix>.*?){{end}}");
var rangeRegexMatches = rangeRegex.Match(template);
while (rangeRegexMatches.Success)
@@ -600,9 +528,13 @@ namespace NzbDrone.Core.Indexers.Cardigann
var expanded = string.Empty;
var all = rangeRegexMatches.Groups[0].Value;
var variable = rangeRegexMatches.Groups[1].Value;
var prefix = rangeRegexMatches.Groups[2].Value;
var postfix = rangeRegexMatches.Groups[3].Value;
var index = rangeRegexMatches.Groups["index"].Value;
var variable = rangeRegexMatches.Groups["variable"].Value;
var prefix = rangeRegexMatches.Groups["prefix"].Value;
var postfix = rangeRegexMatches.Groups["postfix"].Value;
var arrayIndex = 0;
var indexReplace = "{{" + index + "}}";
foreach (var value in (ICollection<string>)variables[variable])
{
@@ -612,7 +544,16 @@ namespace NzbDrone.Core.Indexers.Cardigann
newvalue = modifier(newvalue);
}
expanded += prefix + newvalue + postfix;
var indexValue = arrayIndex++;
if (index.IsNotNullOrWhiteSpace())
{
expanded += prefix.Replace(indexReplace, indexValue.ToString()) + newvalue + postfix.Replace(indexReplace, indexValue.ToString());
}
else
{
expanded += prefix + newvalue + postfix;
}
}
template = template.Replace(all, expanded);

View File

@@ -60,7 +60,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
if (request.SearchPath.Response != null && request.SearchPath.Response.Type.Equals("json"))
{
if (request.SearchPath.Response != null && request.SearchPath.Response.NoResultsMessage != null && (request.SearchPath.Response.NoResultsMessage.Equals(results) || (request.SearchPath.Response.NoResultsMessage == string.Empty && results == string.Empty)))
if (request.SearchPath.Response != null &&
request.SearchPath.Response.NoResultsMessage != null &&
((request.SearchPath.Response.NoResultsMessage != string.Empty && results.Contains(request.SearchPath.Response.NoResultsMessage)) || (request.SearchPath.Response.NoResultsMessage == string.Empty && results == string.Empty)))
{
return releases;
}
@@ -451,7 +453,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
value = release.Description;
break;
case "category":
var cats = MapTrackerCatToNewznab(value);
var cats = _categories.MapTrackerCatToNewznab(value);
if (cats.Any())
{
if (release.Categories == null || fieldModifiers.Contains("noappend"))
@@ -467,7 +469,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
value = release.Categories.ToString();
break;
case "categorydesc":
var catsDesc = MapTrackerCatDescToNewznab(value);
var catsDesc = _categories.MapTrackerCatDescToNewznab(value);
if (catsDesc.Any())
{
if (release.Categories == null || fieldModifiers.Contains("noappend"))
@@ -575,6 +577,13 @@ namespace NzbDrone.Core.Indexers.Cardigann
release.TvdbId = (int)ParseUtil.CoerceLong(tvdbId);
value = release.TvdbId.ToString();
break;
case "doubanid":
var doubanIDRegEx = new Regex(@"(\d+)", RegexOptions.Compiled);
var doubanIDMatch = doubanIDRegEx.Match(value);
var doubanID = doubanIDMatch.Groups[1].Value;
release.DoubanId = (int)ParseUtil.CoerceLong(doubanID);
value = release.DoubanId.ToString();
break;
case "poster":
if (!string.IsNullOrWhiteSpace(value))
{

View File

@@ -91,6 +91,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
variables[".Query.IMDBID"] = searchCriteria.FullImdbId;
variables[".Query.IMDBIDShort"] = searchCriteria.ImdbId;
variables[".Query.TVDBID"] = searchCriteria.TvdbId?.ToString() ?? null;
variables[".Query.TMDBID"] = searchCriteria.TmdbId?.ToString() ?? null;
variables[".Query.TVRageID"] = searchCriteria.RId?.ToString() ?? null;
variables[".Query.TVMazeID"] = searchCriteria.TvMazeId?.ToString() ?? null;
variables[".Query.TraktID"] = searchCriteria.TraktId?.ToString() ?? null;
@@ -975,7 +976,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
{
var search = _definition.Search;
var mappedCategories = MapTorznabCapsToTrackers((int[])variables[".Query.Categories"]);
var mappedCategories = _categories.MapTorznabCapsToTrackers((int[])variables[".Query.Categories"]);
if (mappedCategories.Count == 0)
{
mappedCategories = _defaultCategories;
@@ -1000,7 +1001,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
}
variables[".Query.Keywords"] = string.Join(" ", keywordTokens);
variables[".Keywords"] = ApplyFilters((string)variables[".Query.Keywords"], search.Keywordsfilters);
variables[".Keywords"] = ApplyFilters((string)variables[".Query.Keywords"], search.Keywordsfilters, variables);
// TODO: prepare queries first and then send them parallel
var searchPaths = search.Paths;

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Definitions.Avistaz;
using NzbDrone.Core.Messaging.Events;
@@ -30,15 +30,14 @@ namespace NzbDrone.Core.Indexers.Definitions
};
}
public override IParseIndexerResponse GetParser()
{
return new ExoticaZParser(Capabilities.Categories);
}
protected override IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
MovieSearchParams = new List<MovieSearchParam>
{
MovieSearchParam.Q
}
};
var caps = new IndexerCapabilities();
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.XXXx264, "Video Clip");
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.XXXPack, "Video Pack");
@@ -52,4 +51,21 @@ namespace NzbDrone.Core.Indexers.Definitions
return caps;
}
}
public class ExoticaZParser : AvistazParser
{
private readonly IndexerCapabilitiesCategories _categories;
public ExoticaZParser(IndexerCapabilitiesCategories categories)
{
_categories = categories;
}
protected override List<IndexerCategory> ParseCategories(AvistazRelease row)
{
var cat = row.Category;
return cat.SelectMany(c => _categories.MapTrackerCatToNewznab(c.Key)).ToList();
}
}
}

View File

@@ -26,7 +26,7 @@ namespace NzbDrone.Core.Indexers.FileList
[FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
public string Username { get; set; }
[FieldDefinition(3, Label = "Passkey", HelpText = "Site Passkey", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
[FieldDefinition(3, Label = "Passkey", HelpText = "Site Passkey (This is the alphanumeric string in the tracker url shown in your download client)", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
public string Passkey { get; set; }
public override NzbDroneValidationResult Validate()
@@ -34,28 +34,4 @@ namespace NzbDrone.Core.Indexers.FileList
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
public enum FileListCategories
{
[FieldOption]
Movie_SD = 1,
[FieldOption]
Movie_DVD = 2,
[FieldOption]
Movie_DVDRO = 3,
[FieldOption]
Movie_HD = 4,
[FieldOption]
Movie_HDRO = 19,
[FieldOption]
Movie_BluRay = 20,
[FieldOption]
Movie_BluRay4K = 26,
[FieldOption]
Movie_3D = 25,
[FieldOption]
Movie_4K = 6,
[FieldOption]
Xxx = 7
}
}

View File

@@ -59,7 +59,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
public int TotalSeeders { get; set; }
public int TotalSnatched { get; set; }
public long MaxSize { get; set; }
public long GroupTime { get; set; }
public string GroupTime { get; set; }
public List<GazelleTorrent> Torrents { get; set; }
public bool IsFreeLeech { get; set; }
public bool IsNeutralLeech { get; set; }

View File

@@ -5,6 +5,7 @@ using System.Net;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.Gazelle
@@ -120,7 +121,7 @@ namespace NzbDrone.Core.Indexers.Gazelle
Peers = int.Parse(result.Leechers) + int.Parse(result.Seeders),
Files = result.FileCount,
Grabs = result.Snatches,
PublishDate = DateTimeOffset.FromUnixTimeSeconds(result.GroupTime).UtcDateTime,
PublishDate = long.TryParse(result.GroupTime, out var num) ? DateTimeOffset.FromUnixTimeSeconds(num).UtcDateTime : DateTimeUtil.FromFuzzyTime((string)result.GroupTime),
PosterUrl = posterUrl,
DownloadVolumeFactor = result.IsFreeLeech || result.IsNeutralLeech || result.IsPersonalFreeLeech ? 0 : 1,
UploadVolumeFactor = result.IsNeutralLeech ? 0 : 1

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using AngleSharp.Html.Parser;
using FluentValidation;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
@@ -14,7 +13,6 @@ using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
{
@@ -200,6 +198,11 @@ namespace NzbDrone.Core.Indexers.Definitions
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
if (Settings.UserAgent.IsNotNullOrWhiteSpace())
{
request.HttpRequest.Headers.UserAgent = Settings.UserAgent;
}
yield return request;
}
@@ -355,6 +358,9 @@ namespace NzbDrone.Core.Indexers.Definitions
{
}
[FieldDefinition(2, Label = "Cookie User-Agent", Type = FieldType.Textbox, HelpText = "User-Agent associated with cookie used from Browser")]
public string UserAgent { get; set; }
[FieldDefinition(3, Label = "FreeLeech Only", Type = FieldType.Checkbox, Advanced = true, HelpText = "Search Freeleech torrents only")]
public bool FreeLeechOnly { get; set; }
}

View File

@@ -90,7 +90,7 @@ namespace NzbDrone.Core.Indexers.Newznab
get
{
yield return GetDefinition("abNZB", GetSettings("https://abnzb.com"));
yield return GetDefinition("altHUB", GetSettings("https://althub.co.za"));
yield return GetDefinition("altHUB", GetSettings("https://api.althub.co.za"));
yield return GetDefinition("AnimeTosho (Usenet)", GetSettings("https://feed.animetosho.org"));
yield return GetDefinition("DOGnzb", GetSettings("https://api.dognzb.cr"));
yield return GetDefinition("DrunkenSlug", GetSettings("https://drunkenslug.com"));
@@ -187,7 +187,7 @@ namespace NzbDrone.Core.Indexers.Newznab
}
if (capabilities.TvSearchParams != null &&
new[] { TvSearchParam.Q, TvSearchParam.TvdbId, TvSearchParam.RId }.Any(v => capabilities.TvSearchParams.Contains(v)) &&
new[] { TvSearchParam.Q, TvSearchParam.TvdbId, TvSearchParam.TmdbId, TvSearchParam.RId }.Any(v => capabilities.TvSearchParams.Contains(v)) &&
new[] { TvSearchParam.Season, TvSearchParam.Ep }.All(v => capabilities.TvSearchParams.Contains(v)))
{
return null;

View File

@@ -125,6 +125,11 @@ namespace NzbDrone.Core.Indexers.Newznab
parameters.Add("tvdbid", searchCriteria.TvdbId.Value.ToString());
}
if (searchCriteria.TmdbId.HasValue && capabilities.TvSearchTvdbAvailable)
{
parameters.Add("tmdbid", searchCriteria.TvdbId.Value.ToString());
}
if (searchCriteria.ImdbId.IsNotNullOrWhiteSpace() && capabilities.TvSearchImdbAvailable)
{
parameters.Add("imdbid", searchCriteria.ImdbId);

View File

@@ -96,7 +96,7 @@ namespace NzbDrone.Core.Indexers.Newznab
releaseInfo = base.ProcessItem(item, releaseInfo);
releaseInfo.ImdbId = GetIntAttribute(item, "imdb");
releaseInfo.TmdbId = GetIntAttribute(item, "tmdb");
releaseInfo.TmdbId = GetIntAttribute(item, "tmdbid");
releaseInfo.TvdbId = GetIntAttribute(item, "tvdbid");
releaseInfo.TvRageId = GetIntAttribute(item, "rageid");
releaseInfo.Grabs = GetIntAttribute(item, "grabs");

View File

@@ -1,11 +0,0 @@
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Indexers.PassThePopcorn
{
public class PassThePopcornInfo : TorrentInfo
{
public bool? Golden { get; set; }
public bool? Scene { get; set; }
public bool? Approved { get; set; }
}
}

View File

@@ -94,7 +94,7 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
// Only add approved torrents
try
{
torrentInfos.Add(new PassThePopcornInfo()
torrentInfos.Add(new TorrentInfo()
{
Guid = string.Format("PassThePopcorn-{0}", id),
Title = title,
@@ -104,9 +104,6 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
Seeders = int.Parse(torrent.Seeders),
Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders),
PublishDate = torrent.UploadTime.ToUniversalTime(),
Golden = torrent.GoldenPopcorn,
Scene = torrent.Scene,
Approved = torrent.Checked,
ImdbId = result.ImdbId.IsNotNullOrWhiteSpace() ? int.Parse(result.ImdbId) : 0,
IndexerFlags = flags,
MinimumRatio = 1,

View File

@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.Indexers.PassThePopcorn
{
@@ -37,9 +39,21 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
private IEnumerable<IndexerRequest> GetRequest(string searchParameters)
{
var queryParams = new NameValueCollection
{
{ "action", "advanced" },
{ "json", "noredirect" },
{ "searchstr", searchParameters }
};
if (Settings.FreeleechOnly)
{
queryParams.Add("freetorrent", "1");
}
var request =
new IndexerRequest(
$"{Settings.BaseUrl.Trim().TrimEnd('/')}/torrents.php?action=advanced&json=noredirect&searchstr={searchParameters}",
$"{Settings.BaseUrl.Trim().TrimEnd('/')}/torrents.php?{queryParams.GetQueryString()}",
HttpAccept.Json);
request.HttpRequest.Headers["ApiUser"] = Settings.APIUser;

View File

@@ -22,12 +22,15 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
{
}
[FieldDefinition(2, Label = "APIUser", HelpText = "These settings are found in your PassThePopcorn security settings (Edit Profile > Security).", Privacy = PrivacyLevel.UserName)]
[FieldDefinition(2, Label = "API User", HelpText = "These settings are found in your PassThePopcorn security settings (Edit Profile > Security).", Privacy = PrivacyLevel.UserName)]
public string APIUser { get; set; }
[FieldDefinition(3, Label = "API Key", HelpText = "Site API Key", Privacy = PrivacyLevel.ApiKey)]
public string APIKey { get; set; }
[FieldDefinition(4, Label = "Freeleech Only", HelpText = "Return only freeleech torrents", Type = FieldType.Checkbox)]
public bool FreeleechOnly { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -16,6 +16,7 @@ using NzbDrone.Core.Indexers.Gazelle;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
@@ -276,7 +277,7 @@ namespace NzbDrone.Core.Indexers.Definitions
InfoUrl = infoUrl,
Seeders = int.Parse(result.Seeders),
Peers = int.Parse(result.Leechers) + int.Parse(result.Seeders),
PublishDate = DateTimeOffset.FromUnixTimeSeconds(result.GroupTime).UtcDateTime,
PublishDate = DateTimeOffset.FromUnixTimeSeconds(ParseUtil.CoerceLong(result.GroupTime)).UtcDateTime,
Freeleech = result.IsFreeLeech || result.IsPersonalFreeLeech,
Files = result.FileCount,
Grabs = result.Snatches,

View File

@@ -17,6 +17,7 @@ using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Indexers.Gazelle;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
@@ -182,7 +183,7 @@ namespace NzbDrone.Core.Indexers.Definitions
Peers = int.Parse(result.Leechers) + int.Parse(result.Seeders),
Files = result.FileCount,
Grabs = result.Snatches,
PublishDate = DateTimeOffset.FromUnixTimeSeconds(result.GroupTime).UtcDateTime,
PublishDate = DateTimeOffset.FromUnixTimeSeconds(ParseUtil.CoerceLong(result.GroupTime)).UtcDateTime,
};
var category = result.Category;

View File

@@ -1,183 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Xml;
using FluentValidation;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Settings;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Definitions
{
public class ShowRSS : TorrentIndexerBase<NoAuthTorrentBaseSettings>
{
public override string Name => "ShowRSS";
public override string[] IndexerUrls => new string[] { "https://showrss.info/" };
public override string Language => "en-US";
public override string Description => "showRSS is a service that allows you to keep track of your favorite TV shows";
public override Encoding Encoding => Encoding.UTF8;
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
public override IndexerCapabilities Capabilities => SetCapabilities();
public ShowRSS(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
{
}
public override IIndexerRequestGenerator GetRequestGenerator()
{
return new ShowRSSRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
}
public override IParseIndexerResponse GetParser()
{
return new ShowRSSParser(Settings);
}
private IndexerCapabilities SetCapabilities()
{
var caps = new IndexerCapabilities
{
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
}
};
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TV);
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVSD);
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.TVHD);
return caps;
}
}
public class ShowRSSRequestGenerator : IIndexerRequestGenerator
{
public NoAuthTorrentBaseSettings Settings { get; set; }
public IndexerCapabilities Capabilities { get; set; }
public ShowRSSRequestGenerator()
{
}
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
{
var searchUrl = string.Format("{0}/other/all.rss", Settings.BaseUrl.TrimEnd('/'));
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
yield return request;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
return pageableRequests;
}
public Func<IDictionary<string, string>> GetCookies { get; set; }
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
public class ShowRSSParser : IParseIndexerResponse
{
private readonly NoAuthTorrentBaseSettings _settings;
private string BrowseUrl => _settings.BaseUrl + "browse/";
public ShowRSSParser(NoAuthTorrentBaseSettings settings)
{
_settings = settings;
}
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var torrentInfos = new List<ReleaseInfo>();
var xmlDoc = new XmlDocument();
xmlDoc.LoadXml(indexerResponse.Content);
foreach (XmlNode node in xmlDoc.GetElementsByTagName("item"))
{
var title = node.SelectSingleNode(".//*[local-name()='raw_title']").InnerText;
// TODO: Make sure we don't return all sorts of trash
//if (!query.MatchQueryStringAND(title))
//{
// continue;
//}
var category = title.Contains("720p") || title.Contains("1080p") ?
NewznabStandardCategory.TVHD :
NewznabStandardCategory.TVSD;
var magnetUri = node.SelectSingleNode("link")?.InnerText;
var publishDate = DateTime.Parse(node.SelectSingleNode("pubDate").InnerText, CultureInfo.InvariantCulture);
var infoHash = node.SelectSingleNode(".//*[local-name()='info_hash']").InnerText;
var details = BrowseUrl + node.SelectSingleNode(".//*[local-name()='show_id']").InnerText;
var release = new TorrentInfo
{
Title = title,
InfoUrl = details,
Categories = new List<IndexerCategory> { category },
Guid = magnetUri,
PublishDate = publishDate,
InfoHash = infoHash,
MagnetUrl = magnetUri,
Size = 512,
Seeders = 1,
Peers = 2,
DownloadVolumeFactor = 0,
UploadVolumeFactor = 1
};
torrentInfos.Add(release);
}
return torrentInfos.ToArray();
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
}

View File

@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Indexers.Settings
[FieldDefinition(4)]
public IndexerTorrentBaseSettings TorrentBaseSettings { get; set; } = new IndexerTorrentBaseSettings();
public NzbDroneValidationResult Validate()
public virtual NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NLog.Config;
@@ -117,7 +117,6 @@ namespace NzbDrone.Core.Instrumentation
syslogTarget.MessageSend.Protocol = ProtocolType.Udp;
syslogTarget.MessageSend.Udp.Port = syslogPort;
syslogTarget.MessageSend.Udp.Server = syslogServer;
syslogTarget.MessageSend.Udp.ReconnectInterval = 500;
syslogTarget.MessageCreation.Rfc = RfcNumber.Rfc5424;
syslogTarget.MessageCreation.Rfc5424.AppName = _configFileProvider.InstanceName;

View File

@@ -33,6 +33,7 @@ namespace NzbDrone.Core.Parser.Model
public int ImdbId { get; set; }
public int TmdbId { get; set; }
public int TraktId { get; set; }
public int DoubanId { get; set; }
public int Year { get; set; }
public string Author { get; set; }
public string BookTitle { get; set; }

View File

@@ -3,27 +3,27 @@
<TargetFrameworks>net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AngleSharp.Xml" Version="0.16.0" />
<PackageReference Include="AngleSharp.Xml" Version="0.17.0" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="FluentMigrator.Runner" Version="3.3.2" />
<PackageReference Include="MailKit" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
<PackageReference Include="NLog.Targets.Syslog" Version="6.0.3" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="Npgsql" Version="5.0.11" />
<PackageReference Include="System.Memory" Version="4.5.4" />
<PackageReference Include="System.Memory" Version="4.5.5" />
<PackageReference Include="System.ServiceModel.Syndication" Version="6.0.0" />
<PackageReference Include="FluentMigrator.Runner.SQLite" Version="3.3.2" />
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="3.3.2" />
<PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="4.7.14" />
<PackageReference Include="NLog" Version="5.0.1" />
<PackageReference Include="TinyTwitter" Version="1.1.2" />
<PackageReference Include="Kveer.XmlRPC" Version="1.1.1" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Text.Json" Version="6.0.2" />
<PackageReference Include="System.Text.Json" Version="6.0.5" />
<PackageReference Include="MonoTorrent" Version="2.0.5" />
<PackageReference Include="YamlDotNet" Version="11.2.1" />
<PackageReference Include="AngleSharp" Version="0.16.1" />
<PackageReference Include="AngleSharp" Version="0.17.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Common\Prowlarr.Common.csproj" />

View File

@@ -146,16 +146,24 @@ namespace NzbDrone.Core.Update
_logger.Info("Preparing client");
_diskTransferService.TransferFolder(_appFolderInfo.GetUpdateClientFolder(), updateSandboxFolder, TransferMode.Move);
var updateClientExePath = _appFolderInfo.GetUpdateClientExePath(updatePackage.Runtime);
if (!_diskProvider.FileExists(updateClientExePath))
{
_logger.Warn("Update client {0} does not exist, aborting update.", updateClientExePath);
return false;
}
// Set executable flag on update app
if (OsInfo.IsOsx || (OsInfo.IsLinux && PlatformInfo.IsNetCore))
{
_diskProvider.SetPermissions(_appFolderInfo.GetUpdateClientExePath(updatePackage.Runtime), "0755");
_diskProvider.SetFilePermissions(updateClientExePath, "755", null);
}
_logger.Info("Starting update client {0}", _appFolderInfo.GetUpdateClientExePath(updatePackage.Runtime));
_logger.Info("Starting update client {0}", updateClientExePath);
_logger.ProgressInfo("Prowlarr will restart shortly.");
_processProvider.Start(_appFolderInfo.GetUpdateClientExePath(updatePackage.Runtime), GetUpdaterArgs(updateSandboxFolder));
_processProvider.Start(updateClientExePath, GetUpdaterArgs(updateSandboxFolder));
return true;
}
@@ -298,14 +306,6 @@ namespace NzbDrone.Core.Update
// Check if we have to do an application update on startup
try
{
// Don't do a prestartup update check unless BuiltIn update is enabled
if (_configFileProvider.UpdateAutomatically ||
_configFileProvider.UpdateMechanism != UpdateMechanism.BuiltIn ||
_deploymentInfoProvider.IsExternalUpdateMechanism)
{
return;
}
var updateMarker = Path.Combine(_appFolderInfo.AppDataFolder, "update_required");
if (!_diskProvider.FileExists(updateMarker))
{
@@ -314,6 +314,15 @@ namespace NzbDrone.Core.Update
_logger.Debug("Post-install update check requested");
// Don't do a prestartup update check unless BuiltIn update is enabled
if (!_configFileProvider.UpdateAutomatically ||
_configFileProvider.UpdateMechanism != UpdateMechanism.BuiltIn ||
_deploymentInfoProvider.IsExternalUpdateMechanism)
{
_logger.Debug("Built-in updater disabled, skipping post-install update check");
return;
}
var latestAvailable = _checkUpdateService.AvailableUpdate();
if (latestAvailable == null)
{

View File

@@ -3,11 +3,11 @@ using NzbDrone.Common.Disk;
namespace NzbDrone.Core.Validation
{
public class FileChmodValidator : PropertyValidator
public class FolderChmodValidator : PropertyValidator
{
private readonly IDiskProvider _diskProvider;
public FileChmodValidator(IDiskProvider diskProvider)
public FolderChmodValidator(IDiskProvider diskProvider)
: base("Must contain a valid Unix permissions octal")
{
_diskProvider = diskProvider;
@@ -20,7 +20,7 @@ namespace NzbDrone.Core.Validation
return false;
}
return _diskProvider.IsValidFilePermissionMask(context.PropertyValue.ToString());
return _diskProvider.IsValidFolderPermissionMask(context.PropertyValue.ToString());
}
}
}

View File

@@ -5,13 +5,16 @@ using DryIoc.Microsoft.DependencyInjection;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Jobs;
using NzbDrone.Core.Lifecycle;
@@ -43,6 +46,7 @@ namespace NzbDrone.App.Test
// dummy lifetime and broadcaster so tests resolve
container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object);
container.RegisterInstance<IBroadcastSignalRMessage>(new Mock<IBroadcastSignalRMessage>().Object);
container.RegisterInstance<IOptions<PostgresOptions>>(new Mock<IOptions<PostgresOptions>>().Object);
_container = container.GetServiceProvider();
}
@@ -53,6 +57,12 @@ namespace NzbDrone.App.Test
_container.GetRequiredService<IEnumerable<IIndexer>>().Should().NotBeEmpty();
}
[Test]
public void should_be_able_to_resolve_downloadclients()
{
_container.GetRequiredService<IEnumerable<IDownloadClient>>().Should().NotBeEmpty();
}
[Test]
public void container_should_inject_itself()
{

View File

@@ -9,8 +9,12 @@ using System.Security.Cryptography.X509Certificates;
using System.Text;
using DryIoc;
using DryIoc.Microsoft.DependencyInjection;
using FluentMigrator.Runner.Processors.Postgres;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.WindowsServices;
using NLog;
@@ -23,6 +27,8 @@ using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Host;
using PostgresOptions = NzbDrone.Core.Datastore.PostgresOptions;
namespace NzbDrone.Host
{
@@ -52,6 +58,7 @@ namespace NzbDrone.Host
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var appMode = GetApplicationMode(startupContext);
var config = GetConfiguration(startupContext);
switch (appMode)
{
@@ -80,12 +87,22 @@ namespace NzbDrone.Host
// Utility mode
default:
{
new Container(rules => rules.WithNzbDroneRules())
.AutoAddServices(ASSEMBLIES)
.AddNzbDroneLogger()
.AddStartupContext(startupContext)
.Resolve<UtilityModeRouter>()
.Route(appMode);
new HostBuilder()
.UseServiceProviderFactory(new DryIocServiceProviderFactory(new Container(rules => rules.WithNzbDroneRules())))
.ConfigureContainer<IContainer>(c =>
{
c.AutoAddServices(Bootstrap.ASSEMBLIES)
.AddNzbDroneLogger()
.AddDatabase()
.AddStartupContext(startupContext)
.Resolve<UtilityModeRouter>()
.Route(appMode);
})
.ConfigureServices(services =>
{
services.Configure<PostgresOptions>(config.GetSection("Prowlarr:Postgres"));
}).Build();
break;
}
}
@@ -135,6 +152,15 @@ namespace NzbDrone.Host
.AddDatabase()
.AddStartupContext(context);
})
.ConfigureServices(services =>
{
services.Configure<PostgresOptions>(config.GetSection("Prowlarr:Postgres"));
services.Configure<FormOptions>(x =>
{
//Double the default multipart body length from 128 MB to 256 MB
x.MultipartBodyLengthLimit = 268435456;
});
})
.ConfigureWebHost(builder =>
{
builder.UseConfiguration(config);
@@ -205,6 +231,7 @@ namespace NzbDrone.Host
return new ConfigurationBuilder()
.AddXmlFile(appFolder.GetConfigPath(), optional: true, reloadOnChange: false)
.AddInMemoryCollection(new List<KeyValuePair<string, string>> { new ("dataProtectionFolder", appFolder.GetDataProtectionPath()) })
.AddEnvironmentVariables()
.Build();
}

View File

@@ -4,10 +4,10 @@
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.3.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.3.1" />
<PackageReference Include="DryIoc.dll" Version="4.8.8" />
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="5.1.0" />
</ItemGroup>

View File

@@ -1,9 +1,15 @@
using System.Collections.Generic;
using System.Threading;
using Microsoft.Extensions.Configuration;
using NLog;
using Npgsql;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Core.Indexers.FileList;
using NzbDrone.Test.Common;
using NzbDrone.Test.Common.Datastore;
using Prowlarr.Http.ClientSchema;
namespace NzbDrone.Integration.Test
@@ -19,6 +25,8 @@ namespace NzbDrone.Integration.Test
protected int Port { get; private set; }
protected PostgresOptions PostgresOptions { get; set; } = new ();
protected override string RootUrl => $"http://localhost:{Port}/";
protected override string ApiKey => _runner.ApiKey;
@@ -27,7 +35,14 @@ namespace NzbDrone.Integration.Test
{
Port = Interlocked.Increment(ref StaticPort);
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), Port);
PostgresOptions = PostgresDatabase.GetTestOptions();
if (PostgresOptions?.Host != null)
{
CreatePostgresDb(PostgresOptions);
}
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), PostgresOptions, Port);
_runner.Kill();
_runner.Start();
@@ -56,6 +71,22 @@ namespace NzbDrone.Integration.Test
protected override void StopTestTarget()
{
_runner.Kill();
if (PostgresOptions?.Host != null)
{
DropPostgresDb(PostgresOptions);
}
}
private static void CreatePostgresDb(PostgresOptions options)
{
PostgresDatabase.Create(options, MigrationType.Main);
PostgresDatabase.Create(options, MigrationType.Log);
}
private static void DropPostgresDb(PostgresOptions options)
{
PostgresDatabase.Drop(options, MigrationType.Main);
PostgresDatabase.Drop(options, MigrationType.Log);
}
}
}

View File

@@ -4,7 +4,7 @@
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.3" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.6" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />

View File

@@ -18,11 +18,32 @@ namespace NzbDrone.Mono.Test.DiskProviderTests
[Platform(Exclude = "Win")]
public class DiskProviderFixture : DiskProviderFixtureBase<DiskProvider>
{
private string _tempPath;
public DiskProviderFixture()
{
PosixOnly();
}
[TearDown]
public void MonoDiskProviderFixtureTearDown()
{
// Give ourselves back write permissions so we can delete it
if (_tempPath != null)
{
if (Directory.Exists(_tempPath))
{
Syscall.chmod(_tempPath, FilePermissions.S_IRWXU);
}
else if (File.Exists(_tempPath))
{
Syscall.chmod(_tempPath, FilePermissions.S_IRUSR | FilePermissions.S_IWUSR);
}
_tempPath = null;
}
}
protected override void SetWritePermissions(string path, bool writable)
{
if (Environment.UserName == "root")
@@ -30,16 +51,41 @@ namespace NzbDrone.Mono.Test.DiskProviderTests
Assert.Inconclusive("Need non-root user to test write permissions.");
}
SetWritePermissionsInternal(path, writable, false);
}
protected void SetWritePermissionsInternal(string path, bool writable, bool setgid)
{
// Remove Write permissions, we're still owner so we can clean it up, but we'll have to do that explicitly.
var entry = UnixFileSystemInfo.GetFileSystemEntry(path);
Stat stat;
Syscall.stat(path, out stat);
FilePermissions mode = stat.st_mode;
if (writable)
{
entry.FileAccessPermissions |= FileAccessPermissions.UserWrite | FileAccessPermissions.GroupWrite | FileAccessPermissions.OtherWrite;
mode |= FilePermissions.S_IWUSR | FilePermissions.S_IWGRP | FilePermissions.S_IWOTH;
}
else
{
entry.FileAccessPermissions &= ~(FileAccessPermissions.UserWrite | FileAccessPermissions.GroupWrite | FileAccessPermissions.OtherWrite);
mode &= ~(FilePermissions.S_IWUSR | FilePermissions.S_IWGRP | FilePermissions.S_IWOTH);
}
if (setgid)
{
mode |= FilePermissions.S_ISGID;
}
else
{
mode &= ~FilePermissions.S_ISGID;
}
if (stat.st_mode != mode)
{
if (Syscall.chmod(path, mode) < 0)
{
var error = Stdlib.GetLastError();
throw new LinuxPermissionsException("Error setting group: " + error);
}
}
}
@@ -165,24 +211,25 @@ namespace NzbDrone.Mono.Test.DiskProviderTests
var tempFile = GetTempFilePath();
File.WriteAllText(tempFile, "File1");
SetWritePermissions(tempFile, false);
SetWritePermissionsInternal(tempFile, false, false);
_tempPath = tempFile;
// Verify test setup
Syscall.stat(tempFile, out var fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0444");
Subject.SetPermissions(tempFile, "644");
Subject.SetPermissions(tempFile, "755", null);
Syscall.stat(tempFile, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0644");
Subject.SetPermissions(tempFile, "0644");
Subject.SetPermissions(tempFile, "0755", null);
Syscall.stat(tempFile, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0644");
if (OsInfo.Os != Os.Bsd)
{
// This is not allowed on BSD
Subject.SetPermissions(tempFile, "1664");
Subject.SetPermissions(tempFile, "1775", null);
Syscall.stat(tempFile, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("1664");
}
@@ -194,62 +241,118 @@ namespace NzbDrone.Mono.Test.DiskProviderTests
var tempPath = GetTempFilePath();
Directory.CreateDirectory(tempPath);
SetWritePermissions(tempPath, false);
SetWritePermissionsInternal(tempPath, false, false);
_tempPath = tempPath;
// Verify test setup
Syscall.stat(tempPath, out var fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0555");
Subject.SetPermissions(tempPath, "644");
Subject.SetPermissions(tempPath, "755", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0755");
Subject.SetPermissions(tempPath, "0644");
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0755");
Subject.SetPermissions(tempPath, "1664");
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("1775");
Subject.SetPermissions(tempPath, "775");
Subject.SetPermissions(tempPath, "775", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0775");
Subject.SetPermissions(tempPath, "640");
Subject.SetPermissions(tempPath, "750", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0750");
Subject.SetPermissions(tempPath, "0041");
Subject.SetPermissions(tempPath, "051", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0051");
// reinstate sane permissions so fokder can be cleaned up
Subject.SetPermissions(tempPath, "775");
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0775");
}
[Test]
public void IsValidFilePermissionMask_should_return_correct()
public void should_preserve_setgid_on_set_folder_permissions()
{
// Files may not be executable
Subject.IsValidFilePermissionMask("0777").Should().BeFalse();
Subject.IsValidFilePermissionMask("0544").Should().BeFalse();
Subject.IsValidFilePermissionMask("0454").Should().BeFalse();
Subject.IsValidFilePermissionMask("0445").Should().BeFalse();
var tempPath = GetTempFilePath();
Directory.CreateDirectory(tempPath);
SetWritePermissionsInternal(tempPath, false, true);
_tempPath = tempPath;
// Verify test setup
Syscall.stat(tempPath, out var fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("2555");
Subject.SetPermissions(tempPath, "755", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("2755");
Subject.SetPermissions(tempPath, "775", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("2775");
Subject.SetPermissions(tempPath, "750", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("2750");
Subject.SetPermissions(tempPath, "051", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("2051");
}
[Test]
public void should_clear_setgid_on_set_folder_permissions()
{
var tempPath = GetTempFilePath();
Directory.CreateDirectory(tempPath);
SetWritePermissionsInternal(tempPath, false, true);
_tempPath = tempPath;
// Verify test setup
Syscall.stat(tempPath, out var fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("2555");
Subject.SetPermissions(tempPath, "0755", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0755");
Subject.SetPermissions(tempPath, "0775", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0775");
Subject.SetPermissions(tempPath, "0750", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0750");
Subject.SetPermissions(tempPath, "0051", null);
Syscall.stat(tempPath, out fileStat);
NativeConvert.ToOctalPermissionString(fileStat.st_mode).Should().Be("0051");
}
[Test]
public void IsValidFolderPermissionMask_should_return_correct()
{
// No special bits should be set
Subject.IsValidFilePermissionMask("1644").Should().BeFalse();
Subject.IsValidFilePermissionMask("2644").Should().BeFalse();
Subject.IsValidFilePermissionMask("4644").Should().BeFalse();
Subject.IsValidFilePermissionMask("7644").Should().BeFalse();
Subject.IsValidFolderPermissionMask("1755").Should().BeFalse();
Subject.IsValidFolderPermissionMask("2755").Should().BeFalse();
Subject.IsValidFolderPermissionMask("4755").Should().BeFalse();
Subject.IsValidFolderPermissionMask("7755").Should().BeFalse();
// Files should be readable and writeable by owner
Subject.IsValidFilePermissionMask("0400").Should().BeFalse();
Subject.IsValidFilePermissionMask("0000").Should().BeFalse();
Subject.IsValidFilePermissionMask("0200").Should().BeFalse();
Subject.IsValidFilePermissionMask("0600").Should().BeTrue();
// Folder should be readable and writeable by owner
Subject.IsValidFolderPermissionMask("000").Should().BeFalse();
Subject.IsValidFolderPermissionMask("100").Should().BeFalse();
Subject.IsValidFolderPermissionMask("200").Should().BeFalse();
Subject.IsValidFolderPermissionMask("300").Should().BeFalse();
Subject.IsValidFolderPermissionMask("400").Should().BeFalse();
Subject.IsValidFolderPermissionMask("500").Should().BeFalse();
Subject.IsValidFolderPermissionMask("600").Should().BeFalse();
Subject.IsValidFolderPermissionMask("700").Should().BeTrue();
// Folder should be readable and writeable by owner
Subject.IsValidFolderPermissionMask("0000").Should().BeFalse();
Subject.IsValidFolderPermissionMask("0100").Should().BeFalse();
Subject.IsValidFolderPermissionMask("0200").Should().BeFalse();
Subject.IsValidFolderPermissionMask("0300").Should().BeFalse();
Subject.IsValidFolderPermissionMask("0400").Should().BeFalse();
Subject.IsValidFolderPermissionMask("0500").Should().BeFalse();
Subject.IsValidFolderPermissionMask("0600").Should().BeFalse();
Subject.IsValidFolderPermissionMask("0700").Should().BeTrue();
}
}
}

View File

@@ -1,3 +1,5 @@
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Test.DiskTests;
using NzbDrone.Mono.Disk;
@@ -8,9 +10,20 @@ namespace NzbDrone.Mono.Test.DiskProviderTests
[Platform(Exclude = "Win")]
public class FreeSpaceFixture : FreeSpaceFixtureBase<DiskProvider>
{
public FreeSpaceFixture()
[SetUp]
public void Setup()
{
PosixOnly();
Mocker.SetConstant<IProcMountProvider>(new ProcMountProvider(TestLogger));
Mocker.GetMock<ISymbolicLinkResolver>()
.Setup(v => v.GetCompleteRealPath(It.IsAny<string>()))
.Returns<string>(s => s);
}
[Ignore("Docker")]
[Test]
public void should_be_able_to_check_space_on_ramdrive()
{
Subject.GetAvailableSpace("/run/").Should().NotBe(0);
}
}
}

View File

@@ -8,14 +8,9 @@ using NzbDrone.Test.Common;
namespace NzbDrone.Mono.Test.DiskProviderTests
{
[TestFixture]
[Platform("Mono")]
[Platform(Exclude = "Win")]
public class SymbolicLinkResolverFixture : TestBase<SymbolicLinkResolver>
{
public SymbolicLinkResolverFixture()
{
MonoOnly();
}
[Test]
public void should_follow_nested_symlinks()
{

View File

@@ -61,15 +61,41 @@ namespace NzbDrone.Mono.Disk
{
}
public override void SetPermissions(string path, string mask)
public override void SetFilePermissions(string path, string mask, string group)
{
var permissions = NativeConvert.FromOctalPermissionString(mask);
SetPermissions(path, mask, group, permissions);
}
public override void SetPermissions(string path, string mask, string group)
{
var permissions = NativeConvert.FromOctalPermissionString(mask);
if (File.Exists(path))
{
permissions = GetFilePermissions(permissions);
}
SetPermissions(path, mask, group, permissions);
}
protected void SetPermissions(string path, string mask, string group, FilePermissions permissions)
{
_logger.Debug("Setting permissions: {0} on {1}", mask, path);
var permissions = NativeConvert.FromOctalPermissionString(mask);
if (Directory.Exists(path))
// Preserve non-access permissions
if (Syscall.stat(path, out var curStat) < 0)
{
permissions = GetFolderPermissions(permissions);
var error = Stdlib.GetLastError();
throw new LinuxPermissionsException("Error getting current permissions: " + error);
}
// Preserve existing non-access permissions unless mask is 4 digits
if (mask.Length < 4)
{
permissions |= curStat.st_mode & ~FilePermissions.ACCESSPERMS;
}
if (Syscall.chmod(path, permissions) < 0)
@@ -78,33 +104,39 @@ namespace NzbDrone.Mono.Disk
throw new LinuxPermissionsException("Error setting permissions: " + error);
}
var groupId = GetGroupId(group);
if (Syscall.chown(path, unchecked((uint)-1), groupId) < 0)
{
var error = Stdlib.GetLastError();
throw new LinuxPermissionsException("Error setting group: " + error);
}
}
private static FilePermissions GetFolderPermissions(FilePermissions permissions)
private static FilePermissions GetFilePermissions(FilePermissions permissions)
{
permissions |= (FilePermissions)((int)(permissions & (FilePermissions.S_IRUSR | FilePermissions.S_IRGRP | FilePermissions.S_IROTH)) >> 2);
permissions &= ~(FilePermissions.S_IXUSR | FilePermissions.S_IXGRP | FilePermissions.S_IXOTH);
return permissions;
}
public override bool IsValidFilePermissionMask(string mask)
public override bool IsValidFolderPermissionMask(string mask)
{
try
{
var permissions = NativeConvert.FromOctalPermissionString(mask);
if ((permissions & (FilePermissions.S_ISUID | FilePermissions.S_ISGID | FilePermissions.S_ISVTX)) != 0)
if ((permissions & ~FilePermissions.ACCESSPERMS) != 0)
{
// Only allow access permissions
return false;
}
if ((permissions & (FilePermissions.S_IXUSR | FilePermissions.S_IXGRP | FilePermissions.S_IXOTH)) != 0)
{
return false;
}
if ((permissions & (FilePermissions.S_IRUSR | FilePermissions.S_IWUSR)) != (FilePermissions.S_IRUSR | FilePermissions.S_IWUSR))
if ((permissions & FilePermissions.S_IRWXU) != FilePermissions.S_IRWXU)
{
// We expect at least full owner permissions (700)
return false;
}
@@ -281,9 +313,7 @@ namespace NzbDrone.Mono.Disk
// Catch the exception and attempt to handle these edgecases
// Mono 6.x till 6.10 doesn't properly try use rename first.
if (move &&
((PlatformInfo.Platform == PlatformType.Mono && PlatformInfo.GetVersion() < new Version(6, 10)) ||
(PlatformInfo.Platform == PlatformType.NetCore)))
if (move && (PlatformInfo.Platform == PlatformType.NetCore))
{
if (Syscall.lstat(source, out var sourcestat) == 0 &&
Syscall.lstat(destination, out var deststat) != 0 &&
@@ -311,32 +341,7 @@ namespace NzbDrone.Mono.Disk
var dstInfo = new FileInfo(destination);
var exists = dstInfo.Exists && srcInfo.Exists;
if (PlatformInfo.Platform == PlatformType.Mono && PlatformInfo.GetVersion() >= new Version(6, 6) &&
exists && dstInfo.Length == 0 && srcInfo.Length != 0)
{
// mono >=6.6 bug: zero length file since chmod happens at the start
_logger.Debug("{3} failed to {2} file likely due to known {3} bug, attempting to {2} directly. '{0}' -> '{1}'", source, destination, move ? "move" : "copy", PlatformInfo.PlatformName);
try
{
_logger.Trace("Copying content from {0} to {1} ({2} bytes)", source, destination, srcInfo.Length);
using (var srcStream = new FileStream(source, FileMode.Open, FileAccess.Read))
using (var dstStream = new FileStream(destination, FileMode.Create, FileAccess.Write))
{
srcStream.CopyTo(dstStream);
}
}
catch
{
// If it fails again then bail
throw;
}
}
else if (((PlatformInfo.Platform == PlatformType.Mono &&
PlatformInfo.GetVersion() >= new Version(6, 0) &&
PlatformInfo.GetVersion() < new Version(6, 6)) ||
PlatformInfo.Platform == PlatformType.NetCore) &&
exists && dstInfo.Length == srcInfo.Length)
if (PlatformInfo.Platform == PlatformType.NetCore && exists && dstInfo.Length == srcInfo.Length)
{
// mono 6.0, mono 6.4 and netcore 3.1 bug: full length file since utime and chmod happens at the end
_logger.Debug("{3} failed to {2} file likely due to known {3} bug, attempting to {2} directly. '{0}' -> '{1}'", source, destination, move ? "move" : "copy", PlatformInfo.PlatformName);

View File

@@ -12,6 +12,7 @@ namespace NzbDrone.Mono.Disk
{ "apfs", DriveType.Fixed },
{ "fuse.mergerfs", DriveType.Fixed },
{ "fuse.glusterfs", DriveType.Network },
{ "nullfs", DriveType.Fixed },
{ "zfs", DriveType.Fixed }
};

View File

@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Mono.Posix.NETStandard" Version="5.20.1.34-servarr18" />

View File

@@ -3,17 +3,11 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using DryIoc;
using Moq;
using Moq.Language.Flow;
using NzbDrone.Common.Composition;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Test.Common.AutoMoq.Unity;
using Unity;
[assembly: InternalsVisibleTo("AutoMoq.Tests")]
namespace NzbDrone.Test.Common.AutoMoq
{
@@ -21,32 +15,18 @@ namespace NzbDrone.Test.Common.AutoMoq
public class AutoMoqer
{
public readonly MockBehavior DefaultBehavior = MockBehavior.Default;
public Type ResolveType;
private IUnityContainer _container;
private IContainer _container;
private IDictionary<Type, object> _registeredMocks;
public AutoMoqer()
{
SetupAutoMoqer(new UnityContainer());
}
public AutoMoqer(MockBehavior defaultBehavior)
{
DefaultBehavior = defaultBehavior;
SetupAutoMoqer(new UnityContainer());
}
public AutoMoqer(IUnityContainer container)
{
SetupAutoMoqer(container);
SetupAutoMoqer(CreateTestContainer(new Container()));
}
public virtual T Resolve<T>()
{
ResolveType = typeof(T);
var result = _container.Resolve<T>();
SetConstant(result);
ResolveType = null;
return result;
}
@@ -59,7 +39,6 @@ namespace NzbDrone.Test.Common.AutoMoq
public virtual Mock<T> GetMock<T>(MockBehavior behavior)
where T : class
{
ResolveType = null;
var type = GetTheMockType<T>();
if (GetMockHasNotBeenCalledForThisType(type))
{
@@ -78,87 +57,81 @@ namespace NzbDrone.Test.Common.AutoMoq
public virtual void SetMock(Type type, Mock mock)
{
if (_registeredMocks.ContainsKey(type) == false)
if (GetMockHasNotBeenCalledForThisType(type))
{
_registeredMocks.Add(type, mock);
}
if (mock != null)
{
_container.RegisterInstance(type, mock.Object);
_container.RegisterInstance(type, mock.Object, ifAlreadyRegistered: IfAlreadyRegistered.Replace);
}
}
public virtual void SetConstant<T>(T instance)
{
_container.RegisterInstance(instance);
_container.RegisterInstance(instance, ifAlreadyRegistered: IfAlreadyRegistered.Replace);
SetMock(instance.GetType(), null);
}
public ISetup<T> Setup<T>(Expression<Action<T>> expression)
where T : class
private IContainer CreateTestContainer(IContainer container)
{
return GetMock<T>().Setup(expression);
var c = container.CreateChild(IfAlreadyRegistered.Replace,
container.Rules
.WithDynamicRegistration((serviceType, serviceKey) =>
{
// ignore services with non-default key
if (serviceKey != null)
{
return null;
}
if (serviceType == typeof(object))
{
return null;
}
if (serviceType.IsGenericType && serviceType.IsOpenGeneric())
{
return null;
}
// get the Mock object for the abstract class or interface
if (serviceType.IsInterface || serviceType.IsAbstract)
{
var mockType = typeof(Mock<>).MakeGenericType(serviceType);
var mockFactory = new DelegateFactory(r =>
{
var mock = (Mock)r.Resolve(mockType);
SetMock(serviceType, mock);
return mock.Object;
}, Reuse.Singleton);
return new[] { new DynamicRegistration(mockFactory, IfAlreadyRegistered.Keep) };
}
// concrete types
var concreteTypeFactory = serviceType.ToFactory(Reuse.Singleton, FactoryMethod.ConstructorWithResolvableArgumentsIncludingNonPublic);
return new[] { new DynamicRegistration(concreteTypeFactory) };
},
DynamicRegistrationFlags.Service | DynamicRegistrationFlags.AsFallback));
c.Register(typeof(Mock<>), Reuse.Singleton, FactoryMethod.DefaultConstructor());
return c;
}
public ISetup<T, TResult> Setup<T, TResult>(Expression<Func<T, TResult>> expression)
where T : class
{
return GetMock<T>().Setup(expression);
}
public void Verify<T>(Expression<Action<T>> expression)
where T : class
{
GetMock<T>().Verify(expression);
}
public void Verify<T>(Expression<Action<T>> expression, string failMessage)
where T : class
{
GetMock<T>().Verify(expression, failMessage);
}
public void Verify<T>(Expression<Action<T>> expression, Times times)
where T : class
{
GetMock<T>().Verify(expression, times);
}
public void Verify<T>(Expression<Action<T>> expression, Times times, string failMessage)
where T : class
{
GetMock<T>().Verify(expression, times, failMessage);
}
public void VerifyAllMocks()
{
foreach (var registeredMock in _registeredMocks)
{
var mock = registeredMock.Value as Mock;
if (mock != null)
{
mock.VerifyAll();
}
}
}
private void SetupAutoMoqer(IUnityContainer container)
private void SetupAutoMoqer(IContainer container)
{
_container = container;
container.RegisterInstance(this);
RegisterPlatformLibrary(container);
_registeredMocks = new Dictionary<Type, object>();
AddTheAutoMockingContainerExtensionToTheContainer(container);
AssemblyLoader.RegisterSQLiteResolver();
}
private static void AddTheAutoMockingContainerExtensionToTheContainer(IUnityContainer container)
{
container.AddNewExtension<AutoMockingContainerExtension>();
return;
LoadPlatformLibrary();
AssemblyLoader.RegisterSQLiteResolver();
}
private Mock<T> TheRegisteredMockForThisType<T>(Type type)
@@ -177,7 +150,7 @@ namespace NzbDrone.Test.Common.AutoMoq
private bool GetMockHasNotBeenCalledForThisType(Type type)
{
return _registeredMocks.ContainsKey(type) == false;
return !_registeredMocks.ContainsKey(type);
}
private static Type GetTheMockType<T>()
@@ -186,7 +159,7 @@ namespace NzbDrone.Test.Common.AutoMoq
return typeof(T);
}
private void RegisterPlatformLibrary(IUnityContainer container)
private void LoadPlatformLibrary()
{
var assemblyName = "Prowlarr.Windows";

View File

@@ -1,80 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Moq;
using Unity;
using Unity.Builder;
using Unity.Strategies;
namespace NzbDrone.Test.Common.AutoMoq.Unity
{
public class AutoMockingBuilderStrategy : BuilderStrategy
{
private readonly IUnityContainer _container;
private readonly MockRepository _mockFactory;
private readonly IEnumerable<Type> _registeredTypes;
public AutoMockingBuilderStrategy(IEnumerable<Type> registeredTypes, IUnityContainer container)
{
var autoMoqer = container.Resolve<AutoMoqer>();
_mockFactory = new MockRepository(autoMoqer.DefaultBehavior);
_registeredTypes = registeredTypes;
_container = container;
}
public override void PreBuildUp(ref BuilderContext context)
{
var autoMoqer = _container.Resolve<AutoMoqer>();
var type = GetTheTypeFromTheBuilderContext(context);
if (AMockObjectShouldBeCreatedForThisType(type))
{
var mock = CreateAMockObject(type);
context.Existing = mock.Object;
autoMoqer.SetMock(type, mock);
}
}
private bool AMockObjectShouldBeCreatedForThisType(Type type)
{
var mocker = _container.Resolve<AutoMoqer>();
return TypeIsNotRegistered(type) && (mocker.ResolveType == null || mocker.ResolveType != type);
}
private static Type GetTheTypeFromTheBuilderContext(BuilderContext context)
{
// return (context.OriginalBuildKey).Type;
return context.Type;
}
private bool TypeIsNotRegistered(Type type)
{
return _registeredTypes.Any(x => x.Equals(type)) == false;
}
private Mock CreateAMockObject(Type type)
{
var createMethod = GenerateAnInterfaceMockCreationMethod(type);
return InvokeTheMockCreationMethod(createMethod);
}
private Mock InvokeTheMockCreationMethod(MethodInfo createMethod)
{
return (Mock)createMethod.Invoke(_mockFactory, new object[] { new List<object>().ToArray() });
}
private MethodInfo GenerateAnInterfaceMockCreationMethod(Type type)
{
var createMethodWithNoParameters = _mockFactory.GetType().GetMethod("Create", EmptyArgumentList());
return createMethodWithNoParameters.MakeGenericMethod(new[] { type });
}
private static Type[] EmptyArgumentList()
{
return new[] { typeof(object[]) };
}
}
}

Some files were not shown because too many files have changed in this diff Show More