From e9d8a7c0f81b33c797c3b7832a65022e186945f0 Mon Sep 17 00:00:00 2001 From: Solomon Blount <74916907+siblount@users.noreply.github.com> Date: Sat, 13 Jan 2024 21:23:50 -0500 Subject: [PATCH] Source code maintenance + bug fixes + features (#25) * Minor update to DatabaseView designer. * Fixed #15 Cause for app hang was due to a deadlock while creating connections (ex: waiting for Initialization when called by the Init func). Added new CreateInitialConnection which does not check if the database was initialized and InsertDefaultTables uses the connection instead of null. * Fixed #16 first point - release image handle when done * Clear DPFiles at start & fix file handle leak Fixed the issue where the zip archive file handle was not released even when done; call ReleaseArchiveHandles() at the end of ProcessArchive. Updated 7zArchive to kill the 7z process tree if it still exists. * try..catch..me...if..you..can for all archives try catch ReadMetaFiles, ReadContentFiles and others. * Added sorting combo & functionality. * Order by product name not case sensitive, update limit offset for library queries. * added some remove right click action for library items. * updated remove actions for product record form remove now ask for confirmation remove product is implemented and more advanced than the one on library item. * fix compiler error * fixed noImageFound replaced with random image * Changed logo & app appearance and implemented minor UI perf increase. * Update nuget packages * Added new UpdateProductRecord() & UpdateExtractionRecord() along with helper funcs and proprogating changes across DPDatabase * added delegate w/ 5 args * Added functionality to modify product records in the product record form. Note: Doesn't successfully save. * i have no idea what this is. * added new RestoreTriggers() and TempDeleteTriggers() * fixed RemoveValuesWithCondition(), removed transaction?.Dispose() from finally, typo fixes * removed additional update invocations TableUpdated and ProductRecordModified() already invokes the event, no need for it twice. * Updated UpdateRecordQ() * Fixed #19 - explictly unregister form when closed * Added new properties for DPAbstractFile Also removed FileName for DPAbstractArchive since it will be covered by DPAbstractFile. * Addressed #18, still needs work as content folders are being targetted to DAZ directory root folder. Added new CalculateChildRelativePath() and CalculateChildRelativeTargetPath() funcs for DPFolder. Added GetTargetPath() and propogated changes from DPFolder onto here for DPProcessor. * Remove Queue tab temporarily; still in designer. * fixed missing mutex for app * Minor UI adjustment to DB viewer Anchored button and combobox to the right. * Removed tool strip and other unused UI elements. * Added about form, logo links to about form * slight fix for out of bounds exception thrown here * Added new settings * Settings is now using JSON.Net & using serialization * fixed NoImageFound property returning null * Addresses #21 - tags, etc now have char limits Tags, Product Name, Author, SKU, Folders, Files, errored files, now have char limits. * minor ui change - modified text anchor * fixed settings invalid messasge always showing up * fixed length of n + 1 instead of n * Prevent >70 char limit for tags * Disable thumbnail strip if no image is shown. * update version * remove invalid folder * Split up project into multiple NOTE: You will not be able to build at this commit. * rename namespaces, setup dependencies, and more NOTE: You will not be able to build at this commit. * Split up UsefulFuncs.cs, removed ArrayHelper, Up in PathHelper * moved namespace to DAZ_Installer.Core * moved namespace to DAZ_Installer.Core.External * added some vars from DPCommon into Program * prepared for removal of associations from DPAbstractFile - Flipped dictionary keys and valeus - Now use nodes and listviewitem tags. - Added additional condition for 'Select in hierachy' visibility for list view. - Added select in file list view action. * Added new DPProcessSettings * fuck it just upload everything * Addressed destination not enough space infinite loop when cancelled. * Added new properties to MockedDPIOContext due to some weird NSubstitute errors * Updated ProcessArchiveTest_OutOfStorage to now pass. * Replace NSubstitute with Moq and proprogate changes. Moved Fakes to Test projects instead. * Splitting up DPProcessor - added tag provider module Created new AbstractTagProvider and it's default implementation DPTagProvider. Propogated changes to DPProcessor. * Splitting up DPProcessor - added destination determiner Created an abstract destination determiner to determine which files get extracted and to where. Created it's default implentation at DPDestinationDeterminer. Propogated changes. * got rid of some ugly try-catch blocks * Minor logging update to DPProcessor. * Update SetupProcessor to provide fake DestinationDeterminer and TagProvider * Try-Catch everything in processArchievInternal. * add documentation to (Destination/Temp)HasEnoguhSpace functions * Relay extractor extract progress event * Add testing for processing archive after process. * ._. * Create dotnet-test.yml * delete extra iotests. * cancellation token update (not working atm) * Remove unused variables. * Made adjustments to DPProcessorErrorArgs for clarity. * Clarity for DPArchiveEnterArgs * Added func to skip archives * Update to make extractors cancelable. * Removed redundant CancellationToken. * Process archives iteratively (vs recursively) * Add support for cancelling archive and processing entirely. * Remove skip variable * Add CancellationToken * Make interface for DPDatabase. * Update database to be more async friendly. * Remove unnecessary parameter for ProductRecordAdded * Added locks to DPTaskManager * Return task for RemoveProductRecord. * Update IDPDatabase documentation * Add task execute sequentially helper method. * Add DPDatabase tests. * Fix compile errors. * Fix StringWriter import error. * Update app name to Product Manager for DAZ Studio to avoid trademark and other legal issues :) * fix unintentional hiding for Scope * Search for normalized path of current rar file. * Add test subjects for integration testing. * Fix RAR not extracting files split in multi-volume. * Update test subjects * Skip testing files that are directories. * Always copy test subjects to output directory. * Upload integration tests for RAR Extractor * Check for cancellation at start of readheader loop. This is needed to pass the "PeekTest_CancelledDuringOp" test. But also, it's just better to check for cancellation first thing. * Delete src.rar * Add support for Git LFS in dotnet-test.yml * Cache Nuget Packages, change dotnet version, cd to dir * Cache Nuget Packages, cd to dir before test run * figure out this github action weirdness * Remove action, remove warnings, and update verbosity. * Add logging to figure out github actions weridness * fix dotnet cmd * fix dotnet cmd * add logging to solve this mystery * change verbosity again. * update dotnet-test * Add support for Git LFS in dotnet-test.yml * Update to copy always * Addresses #24 - NormalizePath doesnt trim drive letters * Fix #24 - updated path calculation logic. * Remove duplicate WinApp project. * Update README.md * Rename app * get rid of comment * Fix creating incorrect directory * Implement 7z extractor integration tests * Remove unnecessary test. * Implement zip archive integration tests * More reliable Peek, Extract, and Move events. * Moved stream test helpers * Added new GetRootDirectory() * Fixed relative path issues * fixed destination determiner + add tests * Remove DPIDManager class * Make streams more customizable * Fix bug, add content type as tag, add tests * make helper methods private * Fix this. * Fix logic bugs * change security for constants * Remove extracted check + add comment * Add integration testing for DPProcessor * Add testing for DPDazFile * Add testing for DPDSXFile * Downgrading NuGet packages * Make TargetPath setter public. * Fixed rar extractor extracting incorrectly for select files Fixed DPRarExtractor from extracting other files instead of the selected files if not all of the files were being extracted. * Added tests for partial extraction * Fixed subarchive duplication in destination determiner. * Fixed RAR extractor throwing archive corrupt errors Fixed the RAR extractor throwing archive corrupt errors and added tests and updated FakeRAR based on new findings. If the current mode is Extract, then a Test, Extract, or Skip must be called after calling ReadHeader(), otherwise "Archive is corrupt" will be the exception thrown. * Extract DSX Files instead of DazFiles * Fix path is null exception in ExtractedToTarget * Fix subarchives not being processed + scope fix Moved adding the subarchives to process at the end instead of beginning. Removed combining the destination path with the content folder and redirects since the manifest can skip this behavior since the content folders are for when a manifest does not exist. * fixed subarchives having empty TargetPath + typo If Automatic was an InstallOption, the filePathPart would always be empty since no overridePath is being provided and RelativeTargetPath is empty since the archive is not in a content folder, but is targeted by the algorithm. The subarchives would have an incorrect path only returning the temp path. Additionally, in the DetermineViaFileSense the parent archive's TargetPath was accidentially being set to the child's TargetPath which was incomplete. This resulted in the subarchive having an empty TargetPath. * Created new Testing Suite tool * Target x64 only * Setup real data testing for DPProcessor * Remove logging * Complete real data testing * addressed handling encrypted 7z archives * implement cancel + updated ui responsiveness * fixed not handling encrypted headers correctly * adjust xcopy parameters * fixed success percentage. * Make processor more responsive to cancellations. * update dpprocessor logging * update success method, emit error for bad archive * add more default redirects * update documentation * fix anchor for tabcontrol & cancel not working * added hopefully a working PostPublish target * fix issue with xcopy * remove source files subject to delete action * update archive names after the extract job finished * extract ui optimizations + documentation * use SetProgressBarValue instead * Invoke on CreateProgressCombo. * use calc instead of ref. * remove check if marque is already set * better RecursivelyGetControls func (might remove) * remove useless func * add cancellation support Replaced DPProgressCombo with an actual UserConrtol ProgressCombo. * auto switch page on file drag * ref app info in assembly --- .gitattributes | 8 + .github/workflows/dotnet-test.yml | 6 +- .gitignore | 4 + .vscode/settings.json | 3 + README.md | 5 +- src/.editorconfig | 77 +- src/Custom Controls/Extract.Designer.cs | 321 --- src/Custom Controls/Extract.cs | 273 --- src/Custom Controls/Home.Designer.cs | 229 -- src/Custom Controls/Library.resx | 63 - src/Custom Controls/QueueControl.cs | 23 - src/Custom Controls/Settings.cs | 425 ---- .../DAZ_Installer.Common.projitems | 19 + .../DAZ_Installer.Common.shproj | 13 + src/DAZ_Installer.Common/DPArchiveMap.cs | 16 + .../DPProcessorTestManifest.cs | 32 + src/DAZ_Installer.Common/MSTestLoggerSink.cs | 26 + .../SerilogLoggerConstants.cs | 21 + .../SpanExtensions.cs | 2 +- src/DAZ_Installer.Common/TryHelper.cs | 132 ++ src/DAZ_Installer.Core/.vscode/settings.json | 3 + .../AbstractDestinationDeterminer.cs | 24 + .../Abstractions/AbstractTagProvider.cs | 22 + src/DAZ_Installer.Core/ContentType.cs | 45 + .../DAZ_Installer.Core.csproj | 27 + src/DAZ_Installer.Core/DAZ_Installer.Core.sln | 25 + src/DAZ_Installer.Core/DPAbstractNode.cs | 96 + src/DAZ_Installer.Core/DPArchive.cs | 495 +++++ src/DAZ_Installer.Core/DPArchiveEnterArgs.cs | 16 + src/DAZ_Installer.Core/DPArchiveErrorArgs.cs | 51 + src/DAZ_Installer.Core/DPArchiveExitArgs.cs | 30 + .../DPContentInfo.cs | 19 +- .../DPDSXElement.cs | 43 +- .../DPDSXElementCollection.cs | 50 + src/DAZ_Installer.Core/DPDSXFile.cs | 116 + src/{DP => DAZ_Installer.Core}/DPDSXParser.cs | 77 +- src/{DP => DAZ_Installer.Core}/DPDazFile.cs | 58 +- .../DPDestinationDeterminer.cs | 238 +++ src/DAZ_Installer.Core/DPErrorArgs.cs | 17 + src/DAZ_Installer.Core/DPEventHandler.cs | 8 + src/DAZ_Installer.Core/DPFile.cs | 254 +++ src/DAZ_Installer.Core/DPFolder.cs | 258 +++ src/DAZ_Installer.Core/DPProcessSettings.cs | 85 + src/DAZ_Installer.Core/DPProcessor.cs | 453 ++++ .../DPProcessorErrorArgs.cs | 37 + src/DAZ_Installer.Core/DPProcessorState.cs | 30 + src/DAZ_Installer.Core/DPProductInfo.cs | 44 + src/DAZ_Installer.Core/DPTagProvider.cs | 173 ++ src/DAZ_Installer.Core/DPTaskManager.cs | 320 +++ src/{ => DAZ_Installer.Core}/External/7za.exe | Bin src/{ => DAZ_Installer.Core}/External/RAR.cs | 204 +- .../Extraction/DP7zExtractor.cs | 454 ++++ .../Extraction/DPAbstractExtractor.cs | 198 ++ .../Extraction/DPExtractProgressArgs.cs | 29 + .../Extraction/DPExtractSettings.cs | 44 + .../Extraction/DPExtractionReport.cs | 28 + .../Extraction/DPRARExtractor.cs | 318 +++ .../Extraction/DPZipExtractor.cs | 226 ++ .../Extraction/Factories/ProcessFactory.cs | 7 + .../Extraction/Factories/RARFactory.cs | 9 + .../Factories/ZipArchiveWrapperFactory.cs | 7 + .../Extraction/Interfaces/IProcess.cs | 42 + .../Extraction/Interfaces/IProcessFactory.cs | 7 + .../Extraction/Interfaces/IRAR.cs | 39 + .../Extraction/Interfaces/IRARFactory.cs | 7 + .../Extraction/Interfaces/IZipArchive.cs | 17 + .../Extraction/Interfaces/IZipArchiveEntry.cs | 29 + .../Interfaces/IZipArchiveFactory.cs | 7 + .../Extraction/Wrappers/ProcessWrapper.cs | 45 + .../Wrappers/ZipArchiveEntryWrapper.cs | 36 + .../Extraction/Wrappers/ZipArchiveWrapper.cs | 32 + src/{ => DAZ_Installer.Core}/Libs/UnRAR.dll | Bin .../Properties/AssemblyInfo1.cs | 20 + .../DAZ_Installer.CoreTests.csproj | 59 + src/DAZ_Installer.CoreTests/DPDSXFileTests.cs | 77 + src/DAZ_Installer.CoreTests/DPDazFileTests.cs | 61 + .../DPDestinationDeterminerTests.cs | 434 ++++ .../DPProcessorTests.cs | 271 +++ .../DPTagProviderTests.cs | 188 ++ .../Extraction/DP7zExtractorTests.cs | 540 +++++ .../Extraction/DPRARExtractorTests.cs | 461 ++++ .../Extraction/DPZipExtractorTests.cs | 513 +++++ .../Extraction/Fakes/FakeProcess.cs | 94 + .../Extraction/Fakes/FakeRAR.cs | 143 ++ .../Extraction/Fakes/FakeZipArchive.cs | 45 + .../Extraction/Fakes/FakeZipArchiveEntry.cs | 27 + .../Helpers/DPArchiveTestHelpers.cs | 203 ++ .../Integration/DP7zExtractorTests.cs | 433 ++++ .../Integration/DPRARExtractorTests.cs | 222 ++ .../Integration/DPZipExtractorTests.cs | 230 ++ .../DPDestinationDeterminerTestHelpers.cs | 80 + .../Helpers/DPProcessorTestHelpers.cs | 193 ++ .../DPIntegrationArchiveHelpers.cs | 168 ++ .../Integration/DPProcessorTests.cs | 73 + .../Integration/Test Subjects/Test.rar | 3 + .../Integration/Test Subjects/Test.zip | 3 + .../Test Subjects/Test_split.part1.rar | 3 + .../Test Subjects/Test_split.part2.rar | 3 + .../Test Subjects/Test_split_solid.part1.rar | 3 + .../Test Subjects/Test_split_solid.part2.rar | 3 + .../--INSERT YOUR ARCHIVES HERE--.txt | 1 + .../RealData/DPProcessorTests.cs | 81 + .../--INSERT YOUR MANIFESTS HERE--.txt | 1 + .../RealData/RealDataHelper.cs | 55 + .../DAZ_Installer.Database.csproj | 21 + .../DPDatabase.Abstraction.cs | 494 +++-- .../DPDatabase.Public.cs | 188 ++ .../DPDatabase.QueryProcessing.cs | 146 +- .../DPDatabase.cs | 324 +-- .../DPExtractionRecord.cs | 9 +- .../DPProductRecord.cs | 5 +- .../DPSortMethod.cs | 2 +- .../External/SQLRegexFunction.cs | 10 +- src/DAZ_Installer.Database/IDPDatabase.cs | 191 ++ .../DAZ_Installer.DatabaseTests.csproj | 25 + .../DPDatabaseTests.cs | 305 +++ .../Helpers/DPDatabaseTestHelpers.cs | 162 ++ .../Abstractions/AbstractFileSystem.cs | 60 + .../Abstractions/IContextFactory.cs | 14 + .../Abstractions/IDPDirectoryInfo.cs | 41 + .../Abstractions/IDPDriveInfo.cs | 8 + .../Abstractions/IDPFileInfo.cs | 34 + .../Abstractions/IDPFileScopeSettings.cs | 13 + .../Abstractions/IDPIONode.cs | 34 + .../Abstractions/IDirectoryInfo.cs | 22 + .../Abstractions/IFileInfo.cs | 17 + src/DAZ_Installer.IO/DAZ_Installer.IO.csproj | 9 + src/DAZ_Installer.IO/DPDirectoryInfo.cs | 287 +++ src/DAZ_Installer.IO/DPDriveInfo.cs | 17 + src/DAZ_Installer.IO/DPFileInfo.cs | 306 +++ src/DAZ_Installer.IO/DPFileScopeSettings.cs | 169 ++ src/DAZ_Installer.IO/DPFileSystem.cs | 32 + src/DAZ_Installer.IO/DPIONodeBase.cs | 13 + .../Extensions/DirectoryInfoExtensions.cs | 15 + .../Extensions/FileInfoExtensions.cs | 15 + src/DAZ_Installer.IO/OutOfScopeException.cs | 16 + src/DAZ_Installer.IO/PathHelper.cs | 258 +++ .../PathTransversalException.cs | 20 + .../Properties/AssemblyInfo1.cs | 20 + .../Wrappers/DirectoryInfoWrapper.cs | 40 + .../Wrappers/FileInfoWrapper.cs | 40 + .../DAZ_Installer.IOTests.csproj | 24 + .../DPDirectoryInfoTests.cs | 302 +++ src/DAZ_Installer.IOTests/DPFileInfoTests.cs | 678 ++++++ .../DPFileScopeSettingsTests.cs | 331 +++ .../Fakes/FakeDPDirectoryInfo.cs | 32 + .../Fakes/FakeDPDriveInfo.cs | 19 + .../Fakes/FakeDPFileInfo.cs | 52 + .../Fakes/FakeDirectoryInfo.cs | 69 + .../Fakes/FakeFileInfo.cs | 62 + .../Fakes/FakeFileSystem.cs | 25 + src/DAZ_Installer.IOTests/PathHelperTests.cs | 153 ++ .../PathTransversalExceptionTests.cs | 45 + .../DAZ_Installer.TestingSuiteWindows.csproj | 31 + .../DPDestinationDeterminerEx.cs | 93 + .../MainForm.Designer.cs | 539 +++++ .../MainForm.cs | 682 ++++++ .../MainForm.resx | 132 ++ .../ProcessSettingsDialogue.Designer.cs | 305 +++ .../ProcessSettingsDialogue.cs | 173 ++ .../ProcessSettingsDialogue.resx | 123 ++ .../Program.cs | 34 + .../RecursiveDestinationDeterminer.cs | 27 + .../ResultCompiler.cs | 68 + .../RichTextBoxSink.cs | 29 + src/DAZ_Installer.UI/DAZ_Installer.UI.csproj | 37 + .../LibraryItem.Designer.cs | 0 .../LibraryItem.cs | 69 +- .../LibraryItem.resx | 0 .../LibraryPanel.Designer.cs | 2 +- .../LibraryPanel.cs | 80 +- .../LibraryPanel.resx | 0 .../PageButtonControl.Designer.cs | 0 .../PageButtonControl.cs | 22 +- .../PageButtonControl.resx | 0 .../ProgressCombo.Designer.cs | 102 + src/DAZ_Installer.UI/ProgressCombo.cs | 104 + src/DAZ_Installer.UI/ProgressCombo.resx | 120 ++ .../QueueControl.Designer.cs | 0 src/DAZ_Installer.UI/QueueControl.cs | 10 + .../QueueControl.resx | 0 .../Assets}/ArrowDown.png | Bin .../Assets}/ArrowRight.jpg | Bin .../Assets}/ArrowRight.png | Bin .../DAZ_Installer.Windows/Assets}/ArrowUp.png | Bin src/DAZ_Installer.Windows/Assets/ArrowUp1.png | Bin 0 -> 4365 bytes .../DAZ_Installer.Windows/Assets}/Icon1.ico | Bin .../Assets}/Logo2-256x.png | Bin .../Assets}/NoImageFound.jpg | Bin .../Assets}/RAR-Icon-New-Original-APK.png | Bin .../Assets}/WindowsFolderIcon.png | Bin .../DAZ_Installer.Windows/Assets}/favicon.ico | Bin .../DAZ_Installer.Windows/Assets}/loading.gif | Bin .../DAZ_Installer.Windows/Assets}/logo.png | Bin .../Assets}/thumb_14366704070ZIP.png | Bin .../DAZ_Installer.Windows.csproj | 186 ++ src/DAZ_Installer.Windows/DP/DPExtractJob.cs | 295 +++ .../DP/DPGlobal.cs | 8 +- .../DP/DPNetwork.cs | 43 +- src/DAZ_Installer.Windows/DP/DPRegistry.cs | 63 + src/DAZ_Installer.Windows/DP/DPSettings.cs | 177 ++ .../Forms/AboutForm.Designer.cs | 115 + src/DAZ_Installer.Windows/Forms/AboutForm.cs | 20 + .../Forms/AboutForm.resx | 69 +- .../ContentFolderAliasManager.Designer.cs | 2 +- .../Forms/ContentFolderAliasManager.cs | 38 +- .../Forms/ContentFolderAliasManager.resx | 0 .../Forms/ContentFolderManager.Designer.cs | 2 +- .../Forms/ContentFolderManager.cs | 27 +- .../Forms/ContentFolderManager.resx | 0 .../Forms/DatabaseView.Designer.cs | 2 +- .../Forms/DatabaseView.cs | 16 +- .../Forms/DatabaseView.resx | 0 .../Forms/MainForm.Designer.cs | 254 +++ .../Forms/MainForm.cs | 71 +- src/DAZ_Installer.Windows/Forms/MainForm.resx | 1891 +++++++++++++++++ .../Forms/PasswordInput.Designer.cs | 2 +- .../Forms/PasswordInput.cs | 27 +- .../Forms/PasswordInput.resx | 0 .../Forms/ProductRecordForm.Designer.cs | 4 +- .../Forms/ProductRecordForm.cs | 160 +- .../Forms/ProductRecordForm.resx | 0 .../Forms/TagsManager.Designer.cs | 2 +- .../Forms/TagsManager.cs | 34 +- .../Forms/TagsManager.resx | 0 .../Pages/Extract.Designer.cs | 260 +++ src/DAZ_Installer.Windows/Pages/Extract.cs | 201 ++ .../Pages}/Extract.resx | 64 +- .../Pages/Home.Designer.cs | 220 ++ .../Pages}/Home.cs | 152 +- .../Pages}/Home.resx | 62 +- .../Pages}/Library.Designer.cs | 2 +- .../Pages}/Library.cs | 83 +- .../Pages/Library.resx} | 4 +- .../Pages}/Settings.Designer.cs | 2 +- src/DAZ_Installer.Windows/Pages/Settings.cs | 618 ++++++ .../Pages}/Settings.resx | 0 src/DAZ_Installer.Windows/Program.cs | 91 + .../Properties/Resources - Copy.ignore | 0 .../Properties/Resources.Designer.cs | 14 +- .../Properties/Resources.resx | 13 +- .../Properties/Settings.Designer.cs | 4 +- .../Properties/Settings.settings | 0 .../Resources/Logo2-256x.png | Bin .../Resources/favicon.ico | Bin .../Resources/loading.gif | Bin src/DAZ_Installer.Windows/favicon.ico | Bin 0 -> 105768 bytes src/DAZ_Installer.csproj | 90 - src/DAZ_Installer.sln | 116 +- src/DP/ContentType.cs | 43 - src/DP/DP7zArchive.cs | 339 --- src/DP/DPAbstractArchive.cs | 459 ---- src/DP/DPAbstractFile.cs | 163 -- src/DP/DPCache.cs | 57 - src/DP/DPDSXElementCollection.cs | 54 - src/DP/DPDSXFile.cs | 88 - src/DP/DPDatabase.Public.cs | 334 --- src/DP/DPExtractJob.cs | 111 - src/DP/DPFile.cs | 131 -- src/DP/DPFolder.cs | 267 --- src/DP/DPIDManager.cs | 17 - src/DP/DPProcessor.cs | 443 ---- src/DP/DPProductInfo.cs | 21 - src/DP/DPProgressCombo.cs | 93 - src/DP/DPRARArchive.cs | 404 ---- src/DP/DPRegistry.cs | 55 - src/DP/DPSettings.cs | 200 -- src/DP/DPTaskManager.cs | 276 --- src/DP/DPZipArchive.cs | 202 -- src/DP/Program.cs | 53 - src/DP/UsefulFuncs.cs | 326 --- src/Forms/AboutForm.Designer.cs | 119 -- src/Forms/AboutForm.cs | 20 - src/Forms/MainForm.Designer.cs | 251 --- src/Utilities/ListExtensions.cs | 15 - 275 files changed, 22110 insertions(+), 7179 deletions(-) create mode 100644 .gitattributes create mode 100644 .vscode/settings.json delete mode 100644 src/Custom Controls/Extract.Designer.cs delete mode 100644 src/Custom Controls/Extract.cs delete mode 100644 src/Custom Controls/Home.Designer.cs delete mode 100644 src/Custom Controls/Library.resx delete mode 100644 src/Custom Controls/QueueControl.cs delete mode 100644 src/Custom Controls/Settings.cs create mode 100644 src/DAZ_Installer.Common/DAZ_Installer.Common.projitems create mode 100644 src/DAZ_Installer.Common/DAZ_Installer.Common.shproj create mode 100644 src/DAZ_Installer.Common/DPArchiveMap.cs create mode 100644 src/DAZ_Installer.Common/DPProcessorTestManifest.cs create mode 100644 src/DAZ_Installer.Common/MSTestLoggerSink.cs create mode 100644 src/DAZ_Installer.Common/SerilogLoggerConstants.cs rename src/{Utilities => DAZ_Installer.Common}/SpanExtensions.cs (96%) create mode 100644 src/DAZ_Installer.Common/TryHelper.cs create mode 100644 src/DAZ_Installer.Core/.vscode/settings.json create mode 100644 src/DAZ_Installer.Core/Abstractions/AbstractDestinationDeterminer.cs create mode 100644 src/DAZ_Installer.Core/Abstractions/AbstractTagProvider.cs create mode 100644 src/DAZ_Installer.Core/ContentType.cs create mode 100644 src/DAZ_Installer.Core/DAZ_Installer.Core.csproj create mode 100644 src/DAZ_Installer.Core/DAZ_Installer.Core.sln create mode 100644 src/DAZ_Installer.Core/DPAbstractNode.cs create mode 100644 src/DAZ_Installer.Core/DPArchive.cs create mode 100644 src/DAZ_Installer.Core/DPArchiveEnterArgs.cs create mode 100644 src/DAZ_Installer.Core/DPArchiveErrorArgs.cs create mode 100644 src/DAZ_Installer.Core/DPArchiveExitArgs.cs rename src/{DP => DAZ_Installer.Core}/DPContentInfo.cs (60%) rename src/{DP => DAZ_Installer.Core}/DPDSXElement.cs (52%) create mode 100644 src/DAZ_Installer.Core/DPDSXElementCollection.cs create mode 100644 src/DAZ_Installer.Core/DPDSXFile.cs rename src/{DP => DAZ_Installer.Core}/DPDSXParser.cs (84%) rename src/{DP => DAZ_Installer.Core}/DPDazFile.cs (54%) create mode 100644 src/DAZ_Installer.Core/DPDestinationDeterminer.cs create mode 100644 src/DAZ_Installer.Core/DPErrorArgs.cs create mode 100644 src/DAZ_Installer.Core/DPEventHandler.cs create mode 100644 src/DAZ_Installer.Core/DPFile.cs create mode 100644 src/DAZ_Installer.Core/DPFolder.cs create mode 100644 src/DAZ_Installer.Core/DPProcessSettings.cs create mode 100644 src/DAZ_Installer.Core/DPProcessor.cs create mode 100644 src/DAZ_Installer.Core/DPProcessorErrorArgs.cs create mode 100644 src/DAZ_Installer.Core/DPProcessorState.cs create mode 100644 src/DAZ_Installer.Core/DPProductInfo.cs create mode 100644 src/DAZ_Installer.Core/DPTagProvider.cs create mode 100644 src/DAZ_Installer.Core/DPTaskManager.cs rename src/{ => DAZ_Installer.Core}/External/7za.exe (100%) rename src/{ => DAZ_Installer.Core}/External/RAR.cs (85%) create mode 100644 src/DAZ_Installer.Core/Extraction/DP7zExtractor.cs create mode 100644 src/DAZ_Installer.Core/Extraction/DPAbstractExtractor.cs create mode 100644 src/DAZ_Installer.Core/Extraction/DPExtractProgressArgs.cs create mode 100644 src/DAZ_Installer.Core/Extraction/DPExtractSettings.cs create mode 100644 src/DAZ_Installer.Core/Extraction/DPExtractionReport.cs create mode 100644 src/DAZ_Installer.Core/Extraction/DPRARExtractor.cs create mode 100644 src/DAZ_Installer.Core/Extraction/DPZipExtractor.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Factories/ProcessFactory.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Factories/RARFactory.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Factories/ZipArchiveWrapperFactory.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Interfaces/IProcess.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Interfaces/IProcessFactory.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Interfaces/IRAR.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Interfaces/IRARFactory.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchive.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchiveEntry.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchiveFactory.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Wrappers/ProcessWrapper.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Wrappers/ZipArchiveEntryWrapper.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Wrappers/ZipArchiveWrapper.cs rename src/{ => DAZ_Installer.Core}/Libs/UnRAR.dll (100%) create mode 100644 src/DAZ_Installer.Core/Properties/AssemblyInfo1.cs create mode 100644 src/DAZ_Installer.CoreTests/DAZ_Installer.CoreTests.csproj create mode 100644 src/DAZ_Installer.CoreTests/DPDSXFileTests.cs create mode 100644 src/DAZ_Installer.CoreTests/DPDazFileTests.cs create mode 100644 src/DAZ_Installer.CoreTests/DPDestinationDeterminerTests.cs create mode 100644 src/DAZ_Installer.CoreTests/DPProcessorTests.cs create mode 100644 src/DAZ_Installer.CoreTests/DPTagProviderTests.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/DP7zExtractorTests.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/DPRARExtractorTests.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/DPZipExtractorTests.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeProcess.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeRAR.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeZipArchive.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeZipArchiveEntry.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/Helpers/DPArchiveTestHelpers.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/Integration/DP7zExtractorTests.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/Integration/DPRARExtractorTests.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/Integration/DPZipExtractorTests.cs create mode 100644 src/DAZ_Installer.CoreTests/Helpers/DPDestinationDeterminerTestHelpers.cs create mode 100644 src/DAZ_Installer.CoreTests/Helpers/DPProcessorTestHelpers.cs create mode 100644 src/DAZ_Installer.CoreTests/Integration/DPIntegrationArchiveHelpers.cs create mode 100644 src/DAZ_Installer.CoreTests/Integration/DPProcessorTests.cs create mode 100644 src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test.rar create mode 100644 src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test.zip create mode 100644 src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split.part1.rar create mode 100644 src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split.part2.rar create mode 100644 src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split_solid.part1.rar create mode 100644 src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split_solid.part2.rar create mode 100644 src/DAZ_Installer.CoreTests/RealData/Archives/--INSERT YOUR ARCHIVES HERE--.txt create mode 100644 src/DAZ_Installer.CoreTests/RealData/DPProcessorTests.cs create mode 100644 src/DAZ_Installer.CoreTests/RealData/Manifests/--INSERT YOUR MANIFESTS HERE--.txt create mode 100644 src/DAZ_Installer.CoreTests/RealData/RealDataHelper.cs create mode 100644 src/DAZ_Installer.Database/DAZ_Installer.Database.csproj rename src/{DP => DAZ_Installer.Database}/DPDatabase.Abstraction.cs (76%) create mode 100644 src/DAZ_Installer.Database/DPDatabase.Public.cs rename src/{DP => DAZ_Installer.Database}/DPDatabase.QueryProcessing.cs (59%) rename src/{DP => DAZ_Installer.Database}/DPDatabase.cs (72%) rename src/{DP => DAZ_Installer.Database}/DPExtractionRecord.cs (82%) rename src/{DP => DAZ_Installer.Database}/DPProductRecord.cs (78%) rename src/{DP => DAZ_Installer.Database}/DPSortMethod.cs (87%) rename src/{ => DAZ_Installer.Database}/External/SQLRegexFunction.cs (62%) create mode 100644 src/DAZ_Installer.Database/IDPDatabase.cs create mode 100644 src/DAZ_Installer.DatabaseTests/DAZ_Installer.DatabaseTests.csproj create mode 100644 src/DAZ_Installer.DatabaseTests/DPDatabaseTests.cs create mode 100644 src/DAZ_Installer.DatabaseTests/Helpers/DPDatabaseTestHelpers.cs create mode 100644 src/DAZ_Installer.IO/Abstractions/AbstractFileSystem.cs create mode 100644 src/DAZ_Installer.IO/Abstractions/IContextFactory.cs create mode 100644 src/DAZ_Installer.IO/Abstractions/IDPDirectoryInfo.cs create mode 100644 src/DAZ_Installer.IO/Abstractions/IDPDriveInfo.cs create mode 100644 src/DAZ_Installer.IO/Abstractions/IDPFileInfo.cs create mode 100644 src/DAZ_Installer.IO/Abstractions/IDPFileScopeSettings.cs create mode 100644 src/DAZ_Installer.IO/Abstractions/IDPIONode.cs create mode 100644 src/DAZ_Installer.IO/Abstractions/IDirectoryInfo.cs create mode 100644 src/DAZ_Installer.IO/Abstractions/IFileInfo.cs create mode 100644 src/DAZ_Installer.IO/DAZ_Installer.IO.csproj create mode 100644 src/DAZ_Installer.IO/DPDirectoryInfo.cs create mode 100644 src/DAZ_Installer.IO/DPDriveInfo.cs create mode 100644 src/DAZ_Installer.IO/DPFileInfo.cs create mode 100644 src/DAZ_Installer.IO/DPFileScopeSettings.cs create mode 100644 src/DAZ_Installer.IO/DPFileSystem.cs create mode 100644 src/DAZ_Installer.IO/DPIONodeBase.cs create mode 100644 src/DAZ_Installer.IO/Extensions/DirectoryInfoExtensions.cs create mode 100644 src/DAZ_Installer.IO/Extensions/FileInfoExtensions.cs create mode 100644 src/DAZ_Installer.IO/OutOfScopeException.cs create mode 100644 src/DAZ_Installer.IO/PathHelper.cs create mode 100644 src/DAZ_Installer.IO/PathTransversalException.cs create mode 100644 src/DAZ_Installer.IO/Properties/AssemblyInfo1.cs create mode 100644 src/DAZ_Installer.IO/Wrappers/DirectoryInfoWrapper.cs create mode 100644 src/DAZ_Installer.IO/Wrappers/FileInfoWrapper.cs create mode 100644 src/DAZ_Installer.IOTests/DAZ_Installer.IOTests.csproj create mode 100644 src/DAZ_Installer.IOTests/DPDirectoryInfoTests.cs create mode 100644 src/DAZ_Installer.IOTests/DPFileInfoTests.cs create mode 100644 src/DAZ_Installer.IOTests/DPFileScopeSettingsTests.cs create mode 100644 src/DAZ_Installer.IOTests/Fakes/FakeDPDirectoryInfo.cs create mode 100644 src/DAZ_Installer.IOTests/Fakes/FakeDPDriveInfo.cs create mode 100644 src/DAZ_Installer.IOTests/Fakes/FakeDPFileInfo.cs create mode 100644 src/DAZ_Installer.IOTests/Fakes/FakeDirectoryInfo.cs create mode 100644 src/DAZ_Installer.IOTests/Fakes/FakeFileInfo.cs create mode 100644 src/DAZ_Installer.IOTests/Fakes/FakeFileSystem.cs create mode 100644 src/DAZ_Installer.IOTests/PathHelperTests.cs create mode 100644 src/DAZ_Installer.IOTests/PathTransversalExceptionTests.cs create mode 100644 src/DAZ_Installer.TestingSuiteWindows/DAZ_Installer.TestingSuiteWindows.csproj create mode 100644 src/DAZ_Installer.TestingSuiteWindows/DPDestinationDeterminerEx.cs create mode 100644 src/DAZ_Installer.TestingSuiteWindows/MainForm.Designer.cs create mode 100644 src/DAZ_Installer.TestingSuiteWindows/MainForm.cs create mode 100644 src/DAZ_Installer.TestingSuiteWindows/MainForm.resx create mode 100644 src/DAZ_Installer.TestingSuiteWindows/ProcessSettingsDialogue.Designer.cs create mode 100644 src/DAZ_Installer.TestingSuiteWindows/ProcessSettingsDialogue.cs create mode 100644 src/DAZ_Installer.TestingSuiteWindows/ProcessSettingsDialogue.resx create mode 100644 src/DAZ_Installer.TestingSuiteWindows/Program.cs create mode 100644 src/DAZ_Installer.TestingSuiteWindows/RecursiveDestinationDeterminer.cs create mode 100644 src/DAZ_Installer.TestingSuiteWindows/ResultCompiler.cs create mode 100644 src/DAZ_Installer.TestingSuiteWindows/RichTextBoxSink.cs create mode 100644 src/DAZ_Installer.UI/DAZ_Installer.UI.csproj rename src/{Custom Controls => DAZ_Installer.UI}/LibraryItem.Designer.cs (100%) rename src/{Custom Controls => DAZ_Installer.UI}/LibraryItem.cs (76%) rename src/{Custom Controls => DAZ_Installer.UI}/LibraryItem.resx (100%) rename src/{Custom Controls => DAZ_Installer.UI}/LibraryPanel.Designer.cs (98%) rename src/{Custom Controls => DAZ_Installer.UI}/LibraryPanel.cs (56%) rename src/{Custom Controls => DAZ_Installer.UI}/LibraryPanel.resx (100%) rename src/{Custom Controls => DAZ_Installer.UI}/PageButtonControl.Designer.cs (100%) rename src/{Custom Controls => DAZ_Installer.UI}/PageButtonControl.cs (90%) rename src/{Custom Controls => DAZ_Installer.UI}/PageButtonControl.resx (100%) create mode 100644 src/DAZ_Installer.UI/ProgressCombo.Designer.cs create mode 100644 src/DAZ_Installer.UI/ProgressCombo.cs create mode 100644 src/DAZ_Installer.UI/ProgressCombo.resx rename src/{Custom Controls => DAZ_Installer.UI}/QueueControl.Designer.cs (100%) create mode 100644 src/DAZ_Installer.UI/QueueControl.cs rename src/{Custom Controls => DAZ_Installer.UI}/QueueControl.resx (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/ArrowDown.png (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/ArrowRight.jpg (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/ArrowRight.png (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/ArrowUp.png (100%) create mode 100644 src/DAZ_Installer.Windows/Assets/ArrowUp1.png rename {Assets => src/DAZ_Installer.Windows/Assets}/Icon1.ico (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/Logo2-256x.png (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/NoImageFound.jpg (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/RAR-Icon-New-Original-APK.png (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/WindowsFolderIcon.png (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/favicon.ico (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/loading.gif (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/logo.png (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/thumb_14366704070ZIP.png (100%) create mode 100644 src/DAZ_Installer.Windows/DAZ_Installer.Windows.csproj create mode 100644 src/DAZ_Installer.Windows/DP/DPExtractJob.cs rename src/{ => DAZ_Installer.Windows}/DP/DPGlobal.cs (82%) rename src/{ => DAZ_Installer.Windows}/DP/DPNetwork.cs (71%) create mode 100644 src/DAZ_Installer.Windows/DP/DPRegistry.cs create mode 100644 src/DAZ_Installer.Windows/DP/DPSettings.cs create mode 100644 src/DAZ_Installer.Windows/Forms/AboutForm.Designer.cs create mode 100644 src/DAZ_Installer.Windows/Forms/AboutForm.cs rename src/{ => DAZ_Installer.Windows}/Forms/AboutForm.resx (98%) rename src/{ => DAZ_Installer.Windows}/Forms/ContentFolderAliasManager.Designer.cs (99%) rename src/{ => DAZ_Installer.Windows}/Forms/ContentFolderAliasManager.cs (76%) rename src/{ => DAZ_Installer.Windows}/Forms/ContentFolderAliasManager.resx (100%) rename src/{ => DAZ_Installer.Windows}/Forms/ContentFolderManager.Designer.cs (99%) rename src/{ => DAZ_Installer.Windows}/Forms/ContentFolderManager.cs (86%) rename src/{ => DAZ_Installer.Windows}/Forms/ContentFolderManager.resx (100%) rename src/{ => DAZ_Installer.Windows}/Forms/DatabaseView.Designer.cs (99%) rename src/{ => DAZ_Installer.Windows}/Forms/DatabaseView.cs (76%) rename src/{ => DAZ_Installer.Windows}/Forms/DatabaseView.resx (100%) create mode 100644 src/DAZ_Installer.Windows/Forms/MainForm.Designer.cs rename src/{ => DAZ_Installer.Windows}/Forms/MainForm.cs (70%) create mode 100644 src/DAZ_Installer.Windows/Forms/MainForm.resx rename src/{ => DAZ_Installer.Windows}/Forms/PasswordInput.Designer.cs (99%) rename src/{ => DAZ_Installer.Windows}/Forms/PasswordInput.cs (69%) rename src/{ => DAZ_Installer.Windows}/Forms/PasswordInput.resx (100%) rename src/{ => DAZ_Installer.Windows}/Forms/ProductRecordForm.Designer.cs (99%) rename src/{ => DAZ_Installer.Windows}/Forms/ProductRecordForm.cs (85%) rename src/{ => DAZ_Installer.Windows}/Forms/ProductRecordForm.resx (100%) rename src/{ => DAZ_Installer.Windows}/Forms/TagsManager.Designer.cs (99%) rename src/{ => DAZ_Installer.Windows}/Forms/TagsManager.cs (59%) rename src/{ => DAZ_Installer.Windows}/Forms/TagsManager.resx (100%) create mode 100644 src/DAZ_Installer.Windows/Pages/Extract.Designer.cs create mode 100644 src/DAZ_Installer.Windows/Pages/Extract.cs rename src/{Custom Controls => DAZ_Installer.Windows/Pages}/Extract.resx (71%) create mode 100644 src/DAZ_Installer.Windows/Pages/Home.Designer.cs rename src/{Custom Controls => DAZ_Installer.Windows/Pages}/Home.cs (63%) rename src/{Custom Controls => DAZ_Installer.Windows/Pages}/Home.resx (52%) rename src/{Custom Controls => DAZ_Installer.Windows/Pages}/Library.Designer.cs (99%) rename src/{Custom Controls => DAZ_Installer.Windows/Pages}/Library.cs (84%) rename src/{Forms/MainForm.resx => DAZ_Installer.Windows/Pages/Library.resx} (99%) rename src/{Custom Controls => DAZ_Installer.Windows/Pages}/Settings.Designer.cs (99%) create mode 100644 src/DAZ_Installer.Windows/Pages/Settings.cs rename src/{Custom Controls => DAZ_Installer.Windows/Pages}/Settings.resx (100%) create mode 100644 src/DAZ_Installer.Windows/Program.cs rename src/{ => DAZ_Installer.Windows}/Properties/Resources - Copy.ignore (100%) rename src/{ => DAZ_Installer.Windows}/Properties/Resources.Designer.cs (91%) rename src/{ => DAZ_Installer.Windows}/Properties/Resources.resx (90%) rename src/{ => DAZ_Installer.Windows}/Properties/Settings.Designer.cs (93%) rename src/{ => DAZ_Installer.Windows}/Properties/Settings.settings (100%) rename src/{ => DAZ_Installer.Windows}/Resources/Logo2-256x.png (100%) rename src/{ => DAZ_Installer.Windows}/Resources/favicon.ico (100%) rename src/{ => DAZ_Installer.Windows}/Resources/loading.gif (100%) create mode 100644 src/DAZ_Installer.Windows/favicon.ico delete mode 100644 src/DAZ_Installer.csproj delete mode 100644 src/DP/ContentType.cs delete mode 100644 src/DP/DP7zArchive.cs delete mode 100644 src/DP/DPAbstractArchive.cs delete mode 100644 src/DP/DPAbstractFile.cs delete mode 100644 src/DP/DPCache.cs delete mode 100644 src/DP/DPDSXElementCollection.cs delete mode 100644 src/DP/DPDSXFile.cs delete mode 100644 src/DP/DPDatabase.Public.cs delete mode 100644 src/DP/DPExtractJob.cs delete mode 100644 src/DP/DPFile.cs delete mode 100644 src/DP/DPFolder.cs delete mode 100644 src/DP/DPIDManager.cs delete mode 100644 src/DP/DPProcessor.cs delete mode 100644 src/DP/DPProductInfo.cs delete mode 100644 src/DP/DPProgressCombo.cs delete mode 100644 src/DP/DPRARArchive.cs delete mode 100644 src/DP/DPRegistry.cs delete mode 100644 src/DP/DPSettings.cs delete mode 100644 src/DP/DPTaskManager.cs delete mode 100644 src/DP/DPZipArchive.cs delete mode 100644 src/DP/Program.cs delete mode 100644 src/DP/UsefulFuncs.cs delete mode 100644 src/Forms/AboutForm.Designer.cs delete mode 100644 src/Forms/AboutForm.cs delete mode 100644 src/Forms/MainForm.Designer.cs delete mode 100644 src/Utilities/ListExtensions.cs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bf2d58c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +*.7z.003 filter=lfs diff=lfs merge=lfs -text +*.7z.004 filter=lfs diff=lfs merge=lfs -text +*.7z.005 filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.7z filter=lfs diff=lfs merge=lfs -text +*.7z.001 filter=lfs diff=lfs merge=lfs -text +*.7z.002 filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/dotnet-test.yml b/.github/workflows/dotnet-test.yml index e330ce3..0ad368d 100644 --- a/.github/workflows/dotnet-test.yml +++ b/.github/workflows/dotnet-test.yml @@ -5,9 +5,9 @@ name: .NET Test on: push: - branches: [ "pending-main" ] + branches: [ "pending-main", "main" ] pull_request: - branches: [ "pending-main" ] + branches: [ "pending-main", "main" ] jobs: build: @@ -16,6 +16,8 @@ jobs: steps: - uses: actions/checkout@v3 + with: + lfs: 'true' - name: Setup .NET uses: actions/setup-dotnet@v3 with: diff --git a/.gitignore b/.gitignore index 45956bc..5cd2c1e 100644 --- a/.gitignore +++ b/.gitignore @@ -348,3 +348,7 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ + +# Testing for Daz Product Manager +src/DAZ_Installer.CoreTests/RealData/Archives/* +src/DAZ_Installer.CoreTests/RealData/Manifests/* \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bf6bb4f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "src/DAZ_Installer.sln" +} \ No newline at end of file diff --git a/README.md b/README.md index 79bcaaa..a5e54db 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ -# DAZ Product Installer +# Product Manager for DAZ Studio +[![.NET Test](https://github.com/siblount/DazProductInstaller/actions/workflows/dotnet-test.yml/badge.svg?branch=pending-main)](https://github.com/siblount/DazProductInstaller/actions/workflows/dotnet-test.yml) This is an application currently only for Windows users to install their products regardless of the vendor's format; it accepts common file structures and archive formats. ### Watch a preview here (click on image): -[![Daz Product Installer Alpha Preview Video](https://i.postimg.cc/VL7qpd4B/image.png)](https://www.youtube.com/watch?v=FwLc-dcl8W0) +[![Product Manager for DAZ Studio Alpha Preview Video](https://i.postimg.cc/VL7qpd4B/image.png)](https://www.youtube.com/watch?v=FwLc-dcl8W0) ## What does this project aim to do? Firstly, it aims to be a **free** and **open-source*** application to gain trust of users, allow others to contribute, and release a product that is useful for all. diff --git a/src/.editorconfig b/src/.editorconfig index fe99dc6..349192c 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -7,13 +7,13 @@ csharp_using_directive_placement = outside_namespace:silent csharp_prefer_simple_using_statement = true:suggestion csharp_prefer_braces = true:silent csharp_style_namespace_declarations = block_scoped:silent -csharp_style_expression_bodied_methods = false:silent -csharp_style_expression_bodied_constructors = false:silent -csharp_style_expression_bodied_operators = false:silent -csharp_style_expression_bodied_properties = true:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_accessors = true:silent -csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_methods = when_on_single_line:suggestion +csharp_style_expression_bodied_constructors = when_on_single_line:suggestion +csharp_style_expression_bodied_operators = when_on_single_line:suggestion +csharp_style_expression_bodied_properties = true:suggestion +csharp_style_expression_bodied_indexers = true:suggestion +csharp_style_expression_bodied_accessors = true:suggestion +csharp_style_expression_bodied_lambdas = true:suggestion csharp_style_expression_bodied_local_functions = false:silent csharp_style_throw_expression = true:suggestion csharp_prefer_simple_default_expression = true:suggestion @@ -32,6 +32,23 @@ csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent csharp_style_conditional_delegate_call = true:suggestion +csharp_space_around_binary_operators = before_and_after +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = false:silent +csharp_style_prefer_readonly_struct_member = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion [*.{cs,vb}] #### Naming styles #### @@ -54,46 +71,54 @@ dotnet_naming_rule.private_members_should_be_private_camel.severity = warning dotnet_naming_rule.private_members_should_be_private_camel.symbols = private_members dotnet_naming_rule.private_members_should_be_private_camel.style = private_camel +dotnet_naming_rule.private_non_class_should_be_private_camel.severity = warning +dotnet_naming_rule.private_non_class_should_be_private_camel.symbols = private_non_class +dotnet_naming_rule.private_non_class_should_be_private_camel.style = private_camel + # Symbol specifications dotnet_naming_symbols.interface.applicable_kinds = interface dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = +dotnet_naming_symbols.interface.required_modifiers = dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = +dotnet_naming_symbols.types.required_modifiers = dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = +dotnet_naming_symbols.non_field_members.required_modifiers = dotnet_naming_symbols.private_members.applicable_kinds = property dotnet_naming_symbols.private_members.applicable_accessibilities = private, private_protected -dotnet_naming_symbols.private_members.required_modifers = +dotnet_naming_symbols.private_members.required_modifers = + +dotnet_naming_symbols.private_non_class.applicable_kinds = property, event, method, enum, delegate +dotnet_naming_symbols.private_non_class.applicable_accessibilities = private, private_protected +dotnet_naming_symbols.private_non_class.required_modifers = # Naming styles dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_naming_style.private_camel.required_prefix = _ -dotnet_naming_style.private_camel.required_suffix = -dotnet_naming_style.private_camel.word_separator = +dotnet_naming_style.private_camel.required_prefix = +dotnet_naming_style.private_camel.required_suffix = +dotnet_naming_style.private_camel.word_separator = dotnet_naming_style.private_camel.capitalization = camel_case dotnet_style_operator_placement_when_wrapping = beginning_of_line tab_width = 4 @@ -121,3 +146,11 @@ dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent dotnet_style_allow_multiple_blank_lines_experimental = true:silent dotnet_style_allow_statement_immediately_after_block_experimental = true:silent dotnet_code_quality_unused_parameters = all:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent diff --git a/src/Custom Controls/Extract.Designer.cs b/src/Custom Controls/Extract.Designer.cs deleted file mode 100644 index 5e556e9..0000000 --- a/src/Custom Controls/Extract.Designer.cs +++ /dev/null @@ -1,321 +0,0 @@ - -namespace DAZ_Installer -{ - partial class Extract - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Component Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.components = new System.ComponentModel.Container(); - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Extract)); - this.mainTableLayoutPanel = new System.Windows.Forms.TableLayoutPanel(); - this.tabControl1 = new System.Windows.Forms.TabControl(); - this.fileListPage = new System.Windows.Forms.TabPage(); - this.fileListView = new System.Windows.Forms.ListView(); - this.filePathColumn = new System.Windows.Forms.ColumnHeader(); - this.fileListContextStrip = new System.Windows.Forms.ContextMenuStrip(this.components); - this.inspectFileListMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.selectInHierachyToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.openInExplorerToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.noFilesSelectedToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.fileHierachyPage = new System.Windows.Forms.TabPage(); - this.fileHierachyTree = new System.Windows.Forms.TreeView(); - this.archiveFolderIcons = new System.Windows.Forms.ImageList(this.components); - this.queuePage = new System.Windows.Forms.TabPage(); - this.queueControl1 = new DAZ_Installer.Custom_Controls.QueueControl(); - this.panel1 = new System.Windows.Forms.Panel(); - this.mainProcLbl = new System.Windows.Forms.Label(); - this.fileHierachyContextStrip = new System.Windows.Forms.ContextMenuStrip(this.components); - this.inspectToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.selectInFileListToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.openInExplorerToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); - this.tabControl1.SuspendLayout(); - this.fileListPage.SuspendLayout(); - this.fileListContextStrip.SuspendLayout(); - this.fileHierachyPage.SuspendLayout(); - this.queuePage.SuspendLayout(); - this.panel1.SuspendLayout(); - this.fileHierachyContextStrip.SuspendLayout(); - this.SuspendLayout(); - // - // mainTableLayoutPanel - // - this.mainTableLayoutPanel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.mainTableLayoutPanel.ColumnCount = 1; - this.mainTableLayoutPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.mainTableLayoutPanel.Location = new System.Drawing.Point(31, 65); - this.mainTableLayoutPanel.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.mainTableLayoutPanel.Name = "mainTableLayoutPanel"; - this.mainTableLayoutPanel.RowCount = 1; - this.mainTableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.mainTableLayoutPanel.Size = new System.Drawing.Size(491, 153); - this.mainTableLayoutPanel.TabIndex = 0; - // - // tabControl1 - // - this.tabControl1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.tabControl1.Controls.Add(this.fileListPage); - this.tabControl1.Controls.Add(this.fileHierachyPage); - this.tabControl1.Controls.Add(this.queuePage); - this.tabControl1.Location = new System.Drawing.Point(31, 222); - this.tabControl1.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.tabControl1.Name = "tabControl1"; - this.tabControl1.SelectedIndex = 0; - this.tabControl1.Size = new System.Drawing.Size(491, 97); - this.tabControl1.TabIndex = 1; - // - // fileListPage - // - this.fileListPage.Controls.Add(this.fileListView); - this.fileListPage.Location = new System.Drawing.Point(4, 24); - this.fileListPage.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.fileListPage.Name = "fileListPage"; - this.fileListPage.Padding = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.fileListPage.Size = new System.Drawing.Size(483, 69); - this.fileListPage.TabIndex = 0; - this.fileListPage.Text = "File List"; - this.fileListPage.UseVisualStyleBackColor = true; - // - // fileListView - // - this.fileListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { - this.filePathColumn}); - this.fileListView.ContextMenuStrip = this.fileListContextStrip; - this.fileListView.Dock = System.Windows.Forms.DockStyle.Fill; - this.fileListView.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None; - this.fileListView.Location = new System.Drawing.Point(4, 2); - this.fileListView.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.fileListView.MultiSelect = false; - this.fileListView.Name = "fileListView"; - this.fileListView.Size = new System.Drawing.Size(475, 65); - this.fileListView.TabIndex = 0; - this.fileListView.UseCompatibleStateImageBehavior = false; - this.fileListView.View = System.Windows.Forms.View.Details; - // - // filePathColumn - // - this.filePathColumn.Text = "File Path"; - this.filePathColumn.Width = 530; - // - // fileListContextStrip - // - this.fileListContextStrip.DropShadowEnabled = false; - this.fileListContextStrip.ImageScalingSize = new System.Drawing.Size(20, 20); - this.fileListContextStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.inspectFileListMenuItem, - this.selectInHierachyToolStripMenuItem, - this.openInExplorerToolStripMenuItem, - this.noFilesSelectedToolStripMenuItem}); - this.fileListContextStrip.Name = "contextMenuStrip1"; - this.fileListContextStrip.RenderMode = System.Windows.Forms.ToolStripRenderMode.Professional; - this.fileListContextStrip.ShowImageMargin = false; - this.fileListContextStrip.Size = new System.Drawing.Size(144, 92); - this.fileListContextStrip.Opening += new System.ComponentModel.CancelEventHandler(this.fileListContextStrip_Opening); - // - // inspectFileListMenuItem - // - this.inspectFileListMenuItem.Name = "inspectFileListMenuItem"; - this.inspectFileListMenuItem.Size = new System.Drawing.Size(143, 22); - this.inspectFileListMenuItem.Text = "Inspect"; - this.inspectFileListMenuItem.Visible = false; - // - // selectInHierachyToolStripMenuItem - // - this.selectInHierachyToolStripMenuItem.Name = "selectInHierachyToolStripMenuItem"; - this.selectInHierachyToolStripMenuItem.Size = new System.Drawing.Size(143, 22); - this.selectInHierachyToolStripMenuItem.Text = "Select in Hierachy"; - this.selectInHierachyToolStripMenuItem.Visible = false; - this.selectInHierachyToolStripMenuItem.Click += new System.EventHandler(this.selectInHierachyToolStripMenuItem_Click); - // - // openInExplorerToolStripMenuItem - // - this.openInExplorerToolStripMenuItem.Name = "openInExplorerToolStripMenuItem"; - this.openInExplorerToolStripMenuItem.Size = new System.Drawing.Size(143, 22); - this.openInExplorerToolStripMenuItem.Text = "Open in Explorer"; - this.openInExplorerToolStripMenuItem.Visible = false; - // - // noFilesSelectedToolStripMenuItem - // - this.noFilesSelectedToolStripMenuItem.Enabled = false; - this.noFilesSelectedToolStripMenuItem.Name = "noFilesSelectedToolStripMenuItem"; - this.noFilesSelectedToolStripMenuItem.Size = new System.Drawing.Size(143, 22); - this.noFilesSelectedToolStripMenuItem.Text = "No Files Selected"; - // - // fileHierachyPage - // - this.fileHierachyPage.Controls.Add(this.fileHierachyTree); - this.fileHierachyPage.Location = new System.Drawing.Point(4, 24); - this.fileHierachyPage.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.fileHierachyPage.Name = "fileHierachyPage"; - this.fileHierachyPage.Padding = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.fileHierachyPage.Size = new System.Drawing.Size(483, 69); - this.fileHierachyPage.TabIndex = 1; - this.fileHierachyPage.Text = "File Hierachy"; - this.fileHierachyPage.UseVisualStyleBackColor = true; - // - // fileHierachyTree - // - this.fileHierachyTree.Dock = System.Windows.Forms.DockStyle.Fill; - this.fileHierachyTree.Indent = 21; - this.fileHierachyTree.Location = new System.Drawing.Point(4, 2); - this.fileHierachyTree.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.fileHierachyTree.Name = "fileHierachyTree"; - this.fileHierachyTree.Size = new System.Drawing.Size(475, 65); - this.fileHierachyTree.StateImageList = this.archiveFolderIcons; - this.fileHierachyTree.TabIndex = 0; - // - // archiveFolderIcons - // - this.archiveFolderIcons.ColorDepth = System.Windows.Forms.ColorDepth.Depth8Bit; - this.archiveFolderIcons.ImageStream = ((System.Windows.Forms.ImageListStreamer)(resources.GetObject("archiveFolderIcons.ImageStream"))); - this.archiveFolderIcons.TransparentColor = System.Drawing.SystemColors.Window; - this.archiveFolderIcons.Images.SetKeyName(0, "FolderIcon.png"); - this.archiveFolderIcons.Images.SetKeyName(1, "RARIcon.png"); - this.archiveFolderIcons.Images.SetKeyName(2, "ZIPIcon.png"); - // - // queuePage - // - this.queuePage.Controls.Add(this.queueControl1); - this.queuePage.Location = new System.Drawing.Point(4, 24); - this.queuePage.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.queuePage.Name = "queuePage"; - this.queuePage.Padding = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.queuePage.Size = new System.Drawing.Size(483, 69); - this.queuePage.TabIndex = 2; - this.queuePage.Text = "Queue"; - this.queuePage.UseVisualStyleBackColor = true; - // - // queueControl1 - // - this.queueControl1.Dock = System.Windows.Forms.DockStyle.Fill; - this.queueControl1.Location = new System.Drawing.Point(4, 2); - this.queueControl1.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.queueControl1.Name = "queueControl1"; - this.queueControl1.Size = new System.Drawing.Size(475, 65); - this.queueControl1.TabIndex = 0; - // - // panel1 - // - this.panel1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.panel1.Controls.Add(this.mainProcLbl); - this.panel1.Location = new System.Drawing.Point(31, 22); - this.panel1.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.panel1.Name = "panel1"; - this.panel1.Size = new System.Drawing.Size(486, 40); - this.panel1.TabIndex = 2; - // - // mainProcLbl - // - this.mainProcLbl.AutoEllipsis = true; - this.mainProcLbl.Dock = System.Windows.Forms.DockStyle.Fill; - this.mainProcLbl.Font = new System.Drawing.Font("Segoe UI Variable Display Semil", 17.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.mainProcLbl.Location = new System.Drawing.Point(0, 0); - this.mainProcLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); - this.mainProcLbl.Name = "mainProcLbl"; - this.mainProcLbl.Size = new System.Drawing.Size(486, 40); - this.mainProcLbl.TabIndex = 0; - this.mainProcLbl.Text = "Nothing to extract."; - this.mainProcLbl.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; - this.mainProcLbl.Click += new System.EventHandler(this.mainProcLbl_Click); - // - // fileHierachyContextStrip - // - this.fileHierachyContextStrip.ImageScalingSize = new System.Drawing.Size(20, 20); - this.fileHierachyContextStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.inspectToolStripMenuItem, - this.selectInFileListToolStripMenuItem, - this.openInExplorerToolStripMenuItem1}); - this.fileHierachyContextStrip.Name = "fileHierachyContextStrip"; - this.fileHierachyContextStrip.Size = new System.Drawing.Size(163, 70); - // - // inspectToolStripMenuItem - // - this.inspectToolStripMenuItem.Name = "inspectToolStripMenuItem"; - this.inspectToolStripMenuItem.Size = new System.Drawing.Size(162, 22); - this.inspectToolStripMenuItem.Text = "Inspect"; - // - // selectInFileListToolStripMenuItem - // - this.selectInFileListToolStripMenuItem.Name = "selectInFileListToolStripMenuItem"; - this.selectInFileListToolStripMenuItem.Size = new System.Drawing.Size(162, 22); - this.selectInFileListToolStripMenuItem.Text = "Select in File List"; - // - // openInExplorerToolStripMenuItem1 - // - this.openInExplorerToolStripMenuItem1.Name = "openInExplorerToolStripMenuItem1"; - this.openInExplorerToolStripMenuItem1.Size = new System.Drawing.Size(162, 22); - this.openInExplorerToolStripMenuItem1.Text = "Open in Explorer"; - // - // Extract - // - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Inherit; - this.BackColor = System.Drawing.Color.White; - this.Controls.Add(this.tabControl1); - this.Controls.Add(this.mainTableLayoutPanel); - this.Controls.Add(this.panel1); - this.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.Name = "Extract"; - this.Size = new System.Drawing.Size(542, 344); - this.tabControl1.ResumeLayout(false); - this.fileListPage.ResumeLayout(false); - this.fileListContextStrip.ResumeLayout(false); - this.fileHierachyPage.ResumeLayout(false); - this.queuePage.ResumeLayout(false); - this.panel1.ResumeLayout(false); - this.fileHierachyContextStrip.ResumeLayout(false); - this.ResumeLayout(false); - - } - - #endregion - - private System.Windows.Forms.TableLayoutPanel mainTableLayoutPanel; - private System.Windows.Forms.TabControl tabControl1; - private System.Windows.Forms.TabPage fileListPage; - private System.Windows.Forms.TabPage fileHierachyPage; - private System.Windows.Forms.Panel panel1; - internal System.Windows.Forms.Label mainProcLbl; - private System.Windows.Forms.ListView fileListView; - private System.Windows.Forms.TreeView fileHierachyTree; - private System.Windows.Forms.ColumnHeader filePathColumn; - private System.Windows.Forms.ContextMenuStrip fileListContextStrip; - private System.Windows.Forms.ToolStripMenuItem inspectFileListMenuItem; - private System.Windows.Forms.ToolStripMenuItem selectInHierachyToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem openInExplorerToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem noFilesSelectedToolStripMenuItem; - private System.Windows.Forms.ContextMenuStrip fileHierachyContextStrip; - private System.Windows.Forms.ToolStripMenuItem inspectToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem selectInFileListToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem openInExplorerToolStripMenuItem1; - private System.Windows.Forms.TabPage queuePage; - private Custom_Controls.QueueControl queueControl1; - internal System.Windows.Forms.ImageList archiveFolderIcons; - } -} diff --git a/src/Custom Controls/Extract.cs b/src/Custom Controls/Extract.cs deleted file mode 100644 index 1ef1a63..0000000 --- a/src/Custom Controls/Extract.cs +++ /dev/null @@ -1,273 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE - -using System; -using System.Diagnostics; -using System.Collections.Generic; -using System.IO; -using System.ComponentModel; -using System.Windows.Forms; -using DAZ_Installer.DP; - -namespace DAZ_Installer -{ - - public partial class Extract : UserControl - { - /// - /// Returns the integer of the first available slot in List. Returns -1 if not available. - /// - public static Extract ExtractPage; - internal static Dictionary associatedListItems = new(4096); - internal static Dictionary associatedTreeNodes = new(4096); - - public void resetMainTable() - { - mainTableLayoutPanel.SuspendLayout(); - try - { - if (mainTableLayoutPanel.Controls.Count != 0) - { - var arr = DPCommon.RecursivelyGetControls(mainTableLayoutPanel); - foreach (var control in arr) - { - control.Dispose(); - } - } - } - catch { } - mainTableLayoutPanel.Controls.Clear(); - mainTableLayoutPanel.RowStyles.Clear(); - mainTableLayoutPanel.ColumnCount = 1; - mainTableLayoutPanel.RowStyles.Add(new RowStyle()); - mainTableLayoutPanel.RowCount = 1; - updateMainTableRowSizing(); - mainTableLayoutPanel.ResumeLayout(); - } - public void updateMainTableRowSizing() - { - // TO DO : Invoke. - mainTableLayoutPanel.SuspendLayout(); - float percentageMultiplied = 1f / mainTableLayoutPanel.Controls.Count * 100f; - for (var i = 0; i < mainTableLayoutPanel.RowStyles.Count; i++) - { - mainTableLayoutPanel.RowStyles[i] = new RowStyle(SizeType.Percent, percentageMultiplied); - } - - mainTableLayoutPanel.ResumeLayout(); - mainTableLayoutPanel.Update(); - } - - public DialogResult DoPromptMessage(string message, string title, MessageBoxButtons buttons = MessageBoxButtons.YesNo) - { - - return MessageBox.Show(message, title, buttons, MessageBoxIcon.Hand); - } - - public Extract() - { - InitializeComponent(); - ExtractPage = this; - tabControl1.TabPages.Remove(queuePage); - queuePage.Dispose(); - } - - internal void AddToList(DPAbstractArchive archive) - { - fileListView.BeginUpdate(); - foreach (var content in archive.Contents) - { - var item = fileListView.Items.Add($"{archive.FileName}\\{content.Path}"); - content.AssociatedListItem = item; - associatedListItems.Add(item, content); - } - fileListView.Columns[0].AutoResize(ColumnHeaderAutoResizeStyle.ColumnContent); - fileListView.EndUpdate(); - } - - private void ProcessChildNodes(DPFolder folder, ref TreeNode parentNode) - { - var fileName = Path.GetFileName(folder.Path); - TreeNode folder1 = null; - // We don't need associations for folders. - if (InvokeRequired) - { - folder1 = (TreeNode) Invoke(new Func(parentNode.Nodes.Add), fileName); - AddIcon(folder1, null); - } else - { - folder1 = parentNode.Nodes.Add(fileName); - AddIcon(folder1, null); - } - // Add the DPFiles. - foreach (var file in folder.GetFiles()) - { - fileName = Path.GetFileName(file.Path); - // TO DO: Add condition if file is a DPArchive & extract == true - if (InvokeRequired) - { - var node = (TreeNode) Invoke(new Func(folder1.Nodes.Add), fileName); - file.AssociatedTreeNode = node; - AddIcon(node, file.Ext); - } - else - { - var node = folder1.Nodes.Add(fileName); - file.AssociatedTreeNode = node; - AddIcon(node, file.Ext); - } - } - foreach (var subfolder in folder.subfolders) - { - ProcessChildNodes(subfolder, ref folder1); - } - } - - internal void AddToHierachy(DPAbstractArchive workingArchive) - { - fileHierachyTree.BeginUpdate(); - // Add root node for DPArchive. - var fileName = workingArchive.HierachyName; - TreeNode rootNode = null; - if (InvokeRequired) - { - var func = new Func(fileHierachyTree.Nodes.Add); - rootNode = (TreeNode) Invoke(func,fileName); - workingArchive.AssociatedTreeNode = rootNode; - AddIcon(rootNode, workingArchive.Ext); - - } else - { - rootNode = fileHierachyTree.Nodes.Add(fileName); - workingArchive.AssociatedTreeNode = rootNode; - AddIcon(rootNode, workingArchive.Ext); - } - - - // Add any files that aren't in any folder. - foreach (var file in workingArchive.RootContents) - { - fileName = Path.GetFileName(file.Path); - if (InvokeRequired) - { - var node = (TreeNode) Invoke(new Func(rootNode.Nodes.Add), fileName); - file.AssociatedTreeNode = node; - AddIcon(node, file.Ext); - } else - { - var node = rootNode.Nodes.Add(fileName); - file.AssociatedTreeNode = node; - AddIcon(node, file.Ext); - } - } - - // Recursively add files & folder within each folder. - foreach (var folder in workingArchive.RootFolders) - { - ProcessChildNodes(folder, ref rootNode); - } - fileHierachyTree.ExpandAll(); - fileHierachyTree.EndUpdate(); - } - - // Object to satisfy Invoke. - private void AddIcon(TreeNode node, string ext) - { - if (InvokeRequired) - { - Invoke(AddIcon, node, ext); - } - if (string.IsNullOrEmpty(ext)) - node.StateImageIndex = 0; - else if (ext.Contains("zip") || ext.Contains("7z")) - node.StateImageIndex = 2; - else if (ext.Contains("rar")) - node.StateImageIndex = 1; - } - - public void ResetExtractPage() - { - // Later show nothing to extract panel. - resetMainTable(); - fileListView.Items.Clear(); - fileHierachyTree.Nodes.Clear(); - associatedListItems.Clear(); - associatedTreeNodes.Clear(); - } - /// - /// Creates a progress bar and adds it to the table. [0] - TableLayout, [1] - label, [2] - ProgressBar - /// - /// An array of controls - /// - internal void AddNewProgressCombo(DPProgressCombo combo) { - mainTableLayoutPanel.SuspendLayout(); - if (mainTableLayoutPanel.Controls.Count != 0) - { - mainTableLayoutPanel.RowCount += 1; - mainTableLayoutPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize)); - } - mainTableLayoutPanel.Controls.Add(combo.Panel); - updateMainTableRowSizing(); - mainTableLayoutPanel.ResumeLayout(true); - } - - private void mainProcLbl_Click(object sender, EventArgs e) - { - - } - - #region Handle DPPrecssor Events - internal void DeleteProgressionCombo(DPProgressCombo combo) - { - if (InvokeRequired) - { - Invoke(DeleteProgressionCombo, combo); - return; - } - - mainTableLayoutPanel.SuspendLayout(); - mainTableLayoutPanel.Controls.Remove(combo.Panel); - mainTableLayoutPanel.RowCount = Math.Max(1, mainTableLayoutPanel.Controls.Count); - mainTableLayoutPanel.RowStyles.Clear(); - for (var i = 0; i < mainTableLayoutPanel.RowCount; i++) - mainTableLayoutPanel.RowStyles.Add(new RowStyle()); - mainTableLayoutPanel.ResumeLayout(); - updateMainTableRowSizing(); - } - - #endregion - - #region Context Strip Events - private void selectInHierachyToolStripMenuItem_Click(object sender, EventArgs e) - { - // Get the associated file with listviewitem. - associatedListItems.TryGetValue(fileListView.SelectedItems[0], out DPAbstractFile file); - if (file != null) - { - fileHierachyTree.SelectedNode = file.AssociatedTreeNode; - } - // Switch tab. - tabControl1.SelectTab(fileHierachyPage); - - } - - private void fileListContextStrip_Opening(object sender, CancelEventArgs e) - { - var filesSelected = fileListView.SelectedItems.Count != 0; - inspectFileListMenuItem.Visible = filesSelected; - openInExplorerToolStripMenuItem.Visible = filesSelected; - selectInHierachyToolStripMenuItem.Visible = filesSelected; - noFilesSelectedToolStripMenuItem.Visible = !filesSelected; - } - - public void OpenFileInExplorer(string path) - { - Process.Start(@"explorer.exe", $"/select, \"{path}\""); - } - #endregion - - } - -} - - diff --git a/src/Custom Controls/Home.Designer.cs b/src/Custom Controls/Home.Designer.cs deleted file mode 100644 index 0e58c86..0000000 --- a/src/Custom Controls/Home.Designer.cs +++ /dev/null @@ -1,229 +0,0 @@ - -namespace DAZ_Installer -{ - partial class Home - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Component Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.components = new System.ComponentModel.Container(); - this.titleLbl = new System.Windows.Forms.Label(); - this.extractBtn = new System.Windows.Forms.Button(); - this.addMoreFilesBtn = new System.Windows.Forms.Button(); - this.clearListBtn = new System.Windows.Forms.Button(); - this.listView1 = new System.Windows.Forms.ListView(); - this.columnHeader1 = new System.Windows.Forms.ColumnHeader(); - this.homeListContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components); - this.removeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.addMoreItemsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.openFileDialog1 = new System.Windows.Forms.OpenFileDialog(); - this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); - this.dropBtn = new System.Windows.Forms.Button(); - this.homeListContextMenuStrip.SuspendLayout(); - this.tableLayoutPanel1.SuspendLayout(); - this.SuspendLayout(); - // - // titleLbl - // - this.titleLbl.BackColor = System.Drawing.Color.White; - this.titleLbl.Dock = System.Windows.Forms.DockStyle.Top; - this.titleLbl.Font = new System.Drawing.Font("Segoe UI Variable Text Light", 18F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.titleLbl.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(31)))), ((int)(((byte)(31)))), ((int)(((byte)(31))))); - this.titleLbl.Location = new System.Drawing.Point(0, 0); - this.titleLbl.Name = "titleLbl"; - this.titleLbl.Size = new System.Drawing.Size(542, 55); - this.titleLbl.TabIndex = 0; - this.titleLbl.Text = "Daz Product Installer"; - this.titleLbl.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; - // - // extractBtn - // - this.extractBtn.Dock = System.Windows.Forms.DockStyle.Fill; - this.extractBtn.Location = new System.Drawing.Point(130, 208); - this.extractBtn.Name = "extractBtn"; - this.extractBtn.Size = new System.Drawing.Size(248, 38); - this.extractBtn.TabIndex = 2; - this.extractBtn.Text = "Extract File(s)"; - this.extractBtn.UseVisualStyleBackColor = true; - this.extractBtn.Click += new System.EventHandler(this.button1_Click); - // - // addMoreFilesBtn - // - this.addMoreFilesBtn.Dock = System.Windows.Forms.DockStyle.Fill; - this.addMoreFilesBtn.Location = new System.Drawing.Point(3, 208); - this.addMoreFilesBtn.Name = "addMoreFilesBtn"; - this.addMoreFilesBtn.Size = new System.Drawing.Size(121, 38); - this.addMoreFilesBtn.TabIndex = 4; - this.addMoreFilesBtn.Text = "Add more files"; - this.addMoreFilesBtn.UseVisualStyleBackColor = true; - this.addMoreFilesBtn.Click += new System.EventHandler(this.addMoreFilesBtn_Click); - // - // clearListBtn - // - this.clearListBtn.Dock = System.Windows.Forms.DockStyle.Fill; - this.clearListBtn.Location = new System.Drawing.Point(384, 208); - this.clearListBtn.Name = "clearListBtn"; - this.clearListBtn.Size = new System.Drawing.Size(122, 38); - this.clearListBtn.TabIndex = 3; - this.clearListBtn.Text = "Clear List"; - this.clearListBtn.UseVisualStyleBackColor = true; - this.clearListBtn.Click += new System.EventHandler(this.clearListBtn_Click); - // - // listView1 - // - this.listView1.AllowDrop = true; - this.listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { - this.columnHeader1}); - this.tableLayoutPanel1.SetColumnSpan(this.listView1, 3); - this.listView1.ContextMenuStrip = this.homeListContextMenuStrip; - this.listView1.Dock = System.Windows.Forms.DockStyle.Fill; - this.listView1.ForeColor = System.Drawing.SystemColors.WindowText; - this.listView1.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None; - this.listView1.Location = new System.Drawing.Point(3, 3); - this.listView1.Name = "listView1"; - this.listView1.Size = new System.Drawing.Size(503, 199); - this.listView1.TabIndex = 4; - this.listView1.UseCompatibleStateImageBehavior = false; - this.listView1.View = System.Windows.Forms.View.Details; - this.listView1.DragDrop += new System.Windows.Forms.DragEventHandler(this.dropBtn_DragDrop); - this.listView1.DragEnter += new System.Windows.Forms.DragEventHandler(this.listView1_DragEnter); - // - // columnHeader1 - // - this.columnHeader1.Text = "a"; - this.columnHeader1.Width = 550; - // - // homeListContextMenuStrip - // - this.homeListContextMenuStrip.DropShadowEnabled = false; - this.homeListContextMenuStrip.ImageScalingSize = new System.Drawing.Size(20, 20); - this.homeListContextMenuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.removeToolStripMenuItem, - this.addMoreItemsToolStripMenuItem}); - this.homeListContextMenuStrip.Name = "homeListContextMenuStrip"; - this.homeListContextMenuStrip.ShowImageMargin = false; - this.homeListContextMenuStrip.Size = new System.Drawing.Size(144, 48); - this.homeListContextMenuStrip.Opening += new System.ComponentModel.CancelEventHandler(this.homeListContextMenuStrip_Opening); - // - // removeToolStripMenuItem - // - this.removeToolStripMenuItem.Name = "removeToolStripMenuItem"; - this.removeToolStripMenuItem.Size = new System.Drawing.Size(143, 22); - this.removeToolStripMenuItem.Text = "Remove"; - this.removeToolStripMenuItem.Click += new System.EventHandler(this.removeToolStripMenuItem_Click); - // - // addMoreItemsToolStripMenuItem - // - this.addMoreItemsToolStripMenuItem.Name = "addMoreItemsToolStripMenuItem"; - this.addMoreItemsToolStripMenuItem.Size = new System.Drawing.Size(143, 22); - this.addMoreItemsToolStripMenuItem.Text = "Add more items..."; - this.addMoreItemsToolStripMenuItem.Click += new System.EventHandler(this.addMoreItemsToolStripMenuItem_Click); - // - // openFileDialog1 - // - this.openFileDialog1.AddExtension = false; - this.openFileDialog1.DefaultExt = "zip"; - this.openFileDialog1.Filter = "RAR files (*.rar)|*.rar|ZIP files (*.zip)|*.zip|7z files (*.7z)|*.7z|7z part file" + - " base(*.001)|*.001|All files (*.*)|*.*"; - this.openFileDialog1.Multiselect = true; - this.openFileDialog1.SupportMultiDottedExtensions = true; - // - // tableLayoutPanel1 - // - this.tableLayoutPanel1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.tableLayoutPanel1.ColumnCount = 3; - this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 25F)); - this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); - this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 25F)); - this.tableLayoutPanel1.Controls.Add(this.extractBtn, 1, 1); - this.tableLayoutPanel1.Controls.Add(this.clearListBtn, 2, 1); - this.tableLayoutPanel1.Controls.Add(this.addMoreFilesBtn, 0, 1); - this.tableLayoutPanel1.Controls.Add(this.listView1, 0, 0); - this.tableLayoutPanel1.Location = new System.Drawing.Point(18, 55); - this.tableLayoutPanel1.Name = "tableLayoutPanel1"; - this.tableLayoutPanel1.RowCount = 2; - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 44F)); - this.tableLayoutPanel1.Size = new System.Drawing.Size(509, 249); - this.tableLayoutPanel1.TabIndex = 5; - // - // dropBtn - // - this.dropBtn.AllowDrop = true; - this.dropBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.dropBtn.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(192)))), ((int)(((byte)(255)))), ((int)(((byte)(192))))); - this.dropBtn.Cursor = System.Windows.Forms.Cursors.Hand; - this.dropBtn.FlatAppearance.BorderSize = 0; - this.dropBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.dropBtn.Font = new System.Drawing.Font("Segoe UI Variable Display Semil", 18F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.dropBtn.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(32)))), ((int)(((byte)(32)))), ((int)(((byte)(32))))); - this.dropBtn.Location = new System.Drawing.Point(18, 55); - this.dropBtn.Name = "dropBtn"; - this.dropBtn.Size = new System.Drawing.Size(509, 249); - this.dropBtn.TabIndex = 6; - this.dropBtn.Text = "Click here to select file(s) or drag them here."; - this.dropBtn.UseVisualStyleBackColor = false; - this.dropBtn.Click += new System.EventHandler(this.dropBtn_Click); - this.dropBtn.DragDrop += new System.Windows.Forms.DragEventHandler(this.dropBtn_DragDrop); - this.dropBtn.DragEnter += new System.Windows.Forms.DragEventHandler(this.dropBtn_DragEnter); - this.dropBtn.DragLeave += new System.EventHandler(this.dropBtn_DragLeave); - // - // Home - // - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Inherit; - this.AutoSize = true; - this.BackColor = System.Drawing.Color.White; - this.Controls.Add(this.dropBtn); - this.Controls.Add(this.titleLbl); - this.Controls.Add(this.tableLayoutPanel1); - this.Name = "Home"; - this.Size = new System.Drawing.Size(542, 344); - this.homeListContextMenuStrip.ResumeLayout(false); - this.tableLayoutPanel1.ResumeLayout(false); - this.ResumeLayout(false); - - } - - #endregion - - private System.Windows.Forms.Label titleLbl; - private System.Windows.Forms.Button extractBtn; - private System.Windows.Forms.OpenFileDialog openFileDialog1; - internal System.Windows.Forms.ListView listView1; - private System.Windows.Forms.ContextMenuStrip homeListContextMenuStrip; - private System.Windows.Forms.ToolStripMenuItem removeToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem addMoreItemsToolStripMenuItem; - private System.Windows.Forms.Button addMoreFilesBtn; - private System.Windows.Forms.Button clearListBtn; - private System.Windows.Forms.ColumnHeader columnHeader1; - private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; - private System.Windows.Forms.Button dropBtn; - } -} diff --git a/src/Custom Controls/Library.resx b/src/Custom Controls/Library.resx deleted file mode 100644 index 1632ce3..0000000 --- a/src/Custom Controls/Library.resx +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 214, 17 - - \ No newline at end of file diff --git a/src/Custom Controls/QueueControl.cs b/src/Custom Controls/QueueControl.cs deleted file mode 100644 index d9101cf..0000000 --- a/src/Custom Controls/QueueControl.cs +++ /dev/null @@ -1,23 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Forms; - -namespace DAZ_Installer.Custom_Controls -{ - public partial class QueueControl : UserControl - { - public QueueControl() - { - InitializeComponent(); - } - } -} diff --git a/src/Custom Controls/Settings.cs b/src/Custom Controls/Settings.cs deleted file mode 100644 index d19bdba..0000000 --- a/src/Custom Controls/Settings.cs +++ /dev/null @@ -1,425 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Forms; -using DAZ_Installer.DP; -using DAZ_Installer.Forms; -using Microsoft.VisualBasic.FileIO; - -namespace DAZ_Installer -{ - public partial class Settings : UserControl - { - internal static bool setupComplete { get; set; } = false; - internal static readonly string[] names = new string[] { "Manifest Only", "Manifest and File Sense", "File Sense Only" }; - internal static bool validating { get; set; } = false; - internal static Settings settingsPage { get; set; } = null; - public Settings() - { - InitializeComponent(); - settingsPage = this; - } - - private void Settings_Load(object sender, EventArgs e) - { - Task.Run(LoadSettings); - loadingPanel.Visible = true; - loadingPanel.BringToFront(); - } - - // ._. - private void LoadSettings() - { - DPCommon.WriteToLog("Loading settings..."); - // Get our settings. - DPSettings.currentSettingsObject.Initalize(); - validating = true; - - - SetupDownloadThumbnailsSetting(); - SetupDestinationPathSetting(); - SetupFileHandling(); - SetupTempPath(); - SetupContentFolders(); - SetupContentRedirects(); - SetupDeleteSourceFiles(); - SetupPreviouslyInstalledProducts(); - SetupAllowOverwriting(); - SetupRemoveAction(); - - loadingPanel.Visible = false; - loadingPanel.Dispose(); - validating = false; - - applySettingsBtn.Enabled = DPSettings.invalidSettings; - } - - private void SetupContentRedirects() - { - foreach (var keypair in DPSettings.currentSettingsObject.folderRedirects) - { - contentFolderRedirectsListBox.Items.Add($"{keypair.Key} --> {keypair.Value}"); - } - } - - private void SetupContentFolders() - { - foreach (var folder in DPSettings.currentSettingsObject.commonContentFolderNames) - { - contentFoldersListBox.Items.Add(folder); - } - } - - private void SetupTempPath() - { - tempTxtBox.Text = DPSettings.currentSettingsObject.tempPath; - } - - private void SetupDestinationPathSetting() - { - // If no detected daz content paths, all handled in the initalization phase of DPSettings.currentSettingsObject. - // First, we will add our selected path. - destinationPathCombo.Items.Add(DPSettings.currentSettingsObject.destinationPath); - destinationPathCombo.SelectedIndex = 0; - foreach (var path in DPSettings.currentSettingsObject.detectedDazContentPaths) - { - destinationPathCombo.Items.Add(path); - } - } - - private void SetupFileHandling() - { - - fileHandlingCombo.Items.AddRange(names); - - // Now show the one we selected. - var fileMethod = DPSettings.currentSettingsObject.handleInstallation; - switch (fileMethod) - { - case InstallOptions.ManifestOnly: - fileHandlingCombo.SelectedIndex = 0; - break; - case InstallOptions.ManifestAndAuto: - fileHandlingCombo.SelectedIndex = 1; - break; - case InstallOptions.Automatic: - fileHandlingCombo.SelectedIndex = 2; - break; - } - } - - private void SetupDownloadThumbnailsSetting() - { - - foreach (var option in Enum.GetNames(typeof(SettingOptions))) - { - downloadThumbnailsComboBox.Items.Add(option); - } - - var choice = DPSettings.currentSettingsObject.downloadImages; - downloadThumbnailsComboBox.SelectedItem = Enum.GetName(choice); - } - - private void SetupDeleteSourceFiles() - { - foreach (var option in Enum.GetNames(typeof(SettingOptions))) - { - removeSourceFilesCombo.Items.Add(option); - } - - var choice = DPSettings.currentSettingsObject.permDeleteSource; - removeSourceFilesCombo.SelectedItem = Enum.GetName(choice); - } - - private void SetupPreviouslyInstalledProducts() - { - foreach (var option in Enum.GetNames(typeof(SettingOptions))) - { - installPrevProductsCombo.Items.Add(option); - } - - var choice = DPSettings.currentSettingsObject.installPrevProducts; - installPrevProductsCombo.SelectedItem = Enum.GetName(choice); - } - - private void SetupAllowOverwriting() - { - foreach (var option in Enum.GetNames(typeof(SettingOptions))) - { - allowOverwritingCombo.Items.Add(option); - } - allowOverwritingCombo.SelectedItem = Enum.GetName(DPSettings.currentSettingsObject.OverwriteFiles); - } - - private void SetupRemoveAction() - { - removeActionCombo.Items.AddRange(new string[]{ "Delete permanently", "Move to Recycle Bin"}); - switch (DPSettings.currentSettingsObject.DeleteAction) - { - case RecycleOption.DeletePermanently: - removeActionCombo.SelectedItem = "Delete permanently"; - return; - default: - removeActionCombo.SelectedItem = "Move to Recycle Bin"; - return; - } - } - - private void tableLayoutPanel1_Paint(object sender, PaintEventArgs e) - { - - } - - private void downloadThumbnailsComboBox_SelectedIndexChanged(object sender, EventArgs e) - { - - } - - private void applySettingsBtn_Click(object sender, EventArgs e) - { - // Update settings. - var updateResult = UpdateSettings(); - - if (!updateResult) return; - - // Try saving settings. - var saveResult = DPSettings.currentSettingsObject.SaveSettings(); - - // If something failed... - if (!saveResult) - MessageBox.Show("An error occurred while saving settings. You're settings have NOT been saved. Please try saving again.", - "Error saving settings", MessageBoxButtons.OK, MessageBoxIcon.Error); - - applySettingsBtn.Enabled = false; - } - - private bool UpdateSettings() - { - // We don't update content folders. - if (validating) return false; - var invalidReponses = false; - DPSettings.currentSettingsObject.downloadImages = Enum.Parse((string) downloadThumbnailsComboBox.SelectedItem); - validating = true; - // Destination Path - // A loop occurred when the path was G:/ but G:/ was not mounted. - DESTCHECK: - if (Directory.Exists(destinationPathCombo.Text.Trim())) DPSettings.currentSettingsObject.destinationPath = destinationPathCombo.Text.Trim(); - else { - try - { - Directory.CreateDirectory(tempTxtBox.Text.Trim()); - goto DESTCHECK; - } catch { }; - destinationPathCombo.Text = DPSettings.currentSettingsObject.destinationPath; - invalidReponses = true; - } - - // Temp Path - // TODO: We don't want to delete the temp folder itself. - // For example: We don't want to delete D:/temp, we want to delete D:/temp/. - // The difference is that currently D:/temp will be deleted whereas, - // D:/temp/ will not delete the temp folder but all subfolders and files in it. - TEMPCHECK: - if (Directory.Exists(tempTxtBox.Text.Trim())) DPSettings.currentSettingsObject.tempPath = tempTxtBox.Text.Trim(); - else - { - try - { - Directory.CreateDirectory(tempTxtBox.Text.Trim()); - goto TEMPCHECK; - } catch {} - tempTxtBox.Text = DPSettings.currentSettingsObject.tempPath; - invalidReponses = true; - } - - // File Handling Method - DPSettings.currentSettingsObject.handleInstallation = (InstallOptions)fileHandlingCombo.SelectedIndex; - - //Content Folders - var contentFolders = new HashSet(contentFoldersListBox.Items.Count); - for (var i = 0; i < contentFoldersListBox.Items.Count; i++) - { - contentFolders.Add((string)contentFoldersListBox.Items[i]); - } - DPSettings.currentSettingsObject.commonContentFolderNames = contentFolders; - - // Alias Content Folders - var aliasMap = new Dictionary(contentFolderRedirectsListBox.Items.Count); - foreach (string item in contentFolderRedirectsListBox.Items) - { - var tokens = item.Split(" --> "); - aliasMap[tokens[0]] = tokens[1]; - } - DPSettings.currentSettingsObject.folderRedirects = aliasMap; - - // Permanate Delete Source - DPSettings.currentSettingsObject.permDeleteSource = (SettingOptions)removeSourceFilesCombo.SelectedIndex; - - // Install Prev Products - DPSettings.currentSettingsObject.installPrevProducts = (SettingOptions)installPrevProductsCombo.SelectedIndex; - - if (invalidReponses) - { - MessageBox.Show("Some inputs were invalid and were reset to their previous state. See log for more info.", "Invalid inputs", MessageBoxButtons.OK, MessageBoxIcon.Information); - validating = false; - return false; - } - validating = false; - return true; - } - - private void tempTxtBox_Leave(object sender, EventArgs e) - { - if (!applySettingsBtn.Enabled && tempTxtBox.Text != DPSettings.currentSettingsObject.tempPath) - { - applySettingsBtn.Enabled = true; - } - } - - private void tempTxtBox_KeyUp(object sender, KeyEventArgs e) - { - if (!applySettingsBtn.Enabled && tempTxtBox.Text != DPSettings.currentSettingsObject.tempPath) - { - applySettingsBtn.Enabled = true; - } - } - - private void destinationPathCombo_Leave(object sender, EventArgs e) - { - if (!applySettingsBtn.Enabled && destinationPathCombo.Text != DPSettings.currentSettingsObject.destinationPath) - { - applySettingsBtn.Enabled = true; - } - } - - private void destinationPathCombo_TextChanged(object sender, EventArgs e) - { - if (!applySettingsBtn.Enabled && destinationPathCombo.Text != DPSettings.currentSettingsObject.destinationPath) - { - applySettingsBtn.Enabled = true; - } - } - - private void downloadThumbnailsComboBox_TextChanged(object sender, EventArgs e) - { - if (!applySettingsBtn.Enabled && downloadThumbnailsComboBox.Text != Enum.GetName(DPSettings.currentSettingsObject.downloadImages)) - { - applySettingsBtn.Enabled = true; - } - } - - private void fileHandlingCombo_TextChanged(object sender, EventArgs e) - { - if (!applySettingsBtn.Enabled && fileHandlingCombo.Text != names[(int) DPSettings.currentSettingsObject.handleInstallation]) - { - applySettingsBtn.Enabled = true; - } - } - - private void removeSourceFiles_TextChanged(object sender, EventArgs e) - { - if (!applySettingsBtn.Enabled && removeSourceFilesCombo.Text != Enum.GetName(DPSettings.currentSettingsObject.permDeleteSource)) - { - applySettingsBtn.Enabled = true; - } - } - - private void installPrevProducts_TextChanged(object sender, EventArgs e) - { - if (!applySettingsBtn.Enabled && installPrevProductsCombo.Text != Enum.GetName(DPSettings.currentSettingsObject.installPrevProducts)) - { - applySettingsBtn.Enabled = true; - } - } - - internal string AskForDirectory() - { - if (InvokeRequired) - { - string result = Invoke(new Func(AskForDirectory)); - return result; - } else - { - using (var folderBrowser = new FolderBrowserDialog()) - { - folderBrowser.Description = "Select folder for product installs"; - folderBrowser.UseDescriptionForTitle = true; - var dialogResult = folderBrowser.ShowDialog(); - if (dialogResult == DialogResult.Cancel) return string.Empty; - return folderBrowser.SelectedPath; - } - } - } - - private void chooseDestPathBtn_Click(object sender, EventArgs e) - { - using var browser = new FolderBrowserDialog(); - browser.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyComputer); - browser.Description = "Select folder to install products into"; - browser.UseDescriptionForTitle = true; - var result = browser.ShowDialog(); - if (result == DialogResult.OK) - { - destinationPathCombo.Items[0] = browser.SelectedPath; - destinationPathCombo.SelectedIndex = 0; - destinationPathCombo_TextChanged(null, null); - } - } - - private void chooseTempPathBtn_Click(object sender, EventArgs e) - { - using var browser = new FolderBrowserDialog(); - browser.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyComputer); - browser.Description = "Select folder to temporarily extract products into"; - browser.UseDescriptionForTitle = true; - var result = browser.ShowDialog(); - if (result == DialogResult.OK) - { - tempTxtBox.Text = browser.SelectedPath; - tempTxtBox_Leave(null, null); - } - } - - private void modifyContentFoldersBtn_Click(object sender, EventArgs e) - { - var contentManager = new ContentFolderManager(); - contentManager.ShowDialog(); - contentFoldersListBox.Items.Clear(); - foreach (var item in contentManager.ContentFolders) - { - contentFoldersListBox.Items.Add(item); - } - applySettingsBtn.Enabled = true; - } - - private void modifyContentRedirectsBtn_Click_1(object sender, EventArgs e) - { - var contentManager = new ContentFolderAliasManager(); - contentManager.ShowDialog(); - if (contentManager.AliasListView is null) return; - contentFolderRedirectsListBox.BeginUpdate(); - contentFolderRedirectsListBox.Items.Clear(); - for (var i = 0; i < contentManager.AliasListView.Items.Count; i++) - { - contentFolderRedirectsListBox.Items.Add(contentManager.AliasListView.Items[i].Text); - } - contentFolderRedirectsListBox.EndUpdate(); - applySettingsBtn.Enabled = true; - } - - private void allowOverwritingCombo_TextChanged(object sender, EventArgs e) - { - if (!applySettingsBtn.Enabled && allowOverwritingCombo.Text != Enum.GetName(DPSettings.currentSettingsObject.OverwriteFiles)) - { - applySettingsBtn.Enabled = true; - } - } - - private void openDatabaseBtn_Click(object _, EventArgs __) => new DatabaseView().ShowDialog(); - } -} diff --git a/src/DAZ_Installer.Common/DAZ_Installer.Common.projitems b/src/DAZ_Installer.Common/DAZ_Installer.Common.projitems new file mode 100644 index 0000000..b33c0fa --- /dev/null +++ b/src/DAZ_Installer.Common/DAZ_Installer.Common.projitems @@ -0,0 +1,19 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + b52a5c81-d9c5-4b68-a117-b0aec3dd5d17 + + + DAZ_Installer.Common + + + + + + + + + + \ No newline at end of file diff --git a/src/DAZ_Installer.Common/DAZ_Installer.Common.shproj b/src/DAZ_Installer.Common/DAZ_Installer.Common.shproj new file mode 100644 index 0000000..3acf3aa --- /dev/null +++ b/src/DAZ_Installer.Common/DAZ_Installer.Common.shproj @@ -0,0 +1,13 @@ + + + + b52a5c81-d9c5-4b68-a117-b0aec3dd5d17 + 14.0 + + + + + + + + diff --git a/src/DAZ_Installer.Common/DPArchiveMap.cs b/src/DAZ_Installer.Common/DPArchiveMap.cs new file mode 100644 index 0000000..4e2f585 --- /dev/null +++ b/src/DAZ_Installer.Common/DPArchiveMap.cs @@ -0,0 +1,16 @@ +using DAZ_Installer.Core; +using System.Collections.Generic; +using System.Text.Json; + +public struct DPArchiveMap +{ + public string ArchiveName { get; set; } + public Dictionary Mappings; + + public DPArchiveMap(string archiveName, Dictionary mappings) + { + ArchiveName = archiveName; + Mappings = mappings; + } + +} \ No newline at end of file diff --git a/src/DAZ_Installer.Common/DPProcessorTestManifest.cs b/src/DAZ_Installer.Common/DPProcessorTestManifest.cs new file mode 100644 index 0000000..8d9c347 --- /dev/null +++ b/src/DAZ_Installer.Common/DPProcessorTestManifest.cs @@ -0,0 +1,32 @@ +using DAZ_Installer.Core; +using System.Collections.Generic; +using System.Text.Json; + +public struct DPProcessorTestManifest +{ + public DPProcessSettings Settings; + public string ArchiveName; + public List Results; + + public static readonly JsonSerializerOptions options = new() + { + IncludeFields = true, + WriteIndented = true, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, + IgnoreReadOnlyProperties = false, + IgnoreReadOnlyFields = false, + }; + + public DPProcessorTestManifest(DPProcessSettings settings, string name, List results) + { + Settings = settings; + ArchiveName = name; + Results = results; + } + + public string ToJson() => JsonSerializer.Serialize(this, options); + + public static DPProcessorTestManifest FromJson(string json) => JsonSerializer.Deserialize(json, options); + + public override string ToString() => ToJson(); +} \ No newline at end of file diff --git a/src/DAZ_Installer.Common/MSTestLoggerSink.cs b/src/DAZ_Installer.Common/MSTestLoggerSink.cs new file mode 100644 index 0000000..76815a0 --- /dev/null +++ b/src/DAZ_Installer.Common/MSTestLoggerSink.cs @@ -0,0 +1,26 @@ +using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting; +using System.IO; +using System; + +namespace DAZ_Installer +{ + internal class MSTestLoggerSink : ILogEventSink + { + private ITextFormatter formatter = SerilogLoggerConstants.Template; + Action logMessageFunc; + // We can't directly do this because the namespace is not exposed to non-testing solutions. + public MSTestLoggerSink(ITextFormatter? formatter = null, Action logMessageFunc = null) + { + if (formatter != null) this.formatter = formatter; + this.logMessageFunc = logMessageFunc ?? throw new ArgumentNullException(nameof(logMessageFunc)); + } + public void Emit(LogEvent logEvent) + { + using StringWriter stringWriter = new StringWriter(); + formatter.Format(logEvent, stringWriter); + logMessageFunc(stringWriter.ToString(), Array.Empty()); + } + } +} diff --git a/src/DAZ_Installer.Common/SerilogLoggerConstants.cs b/src/DAZ_Installer.Common/SerilogLoggerConstants.cs new file mode 100644 index 0000000..47c9d4f --- /dev/null +++ b/src/DAZ_Installer.Common/SerilogLoggerConstants.cs @@ -0,0 +1,21 @@ +using Serilog.Templates; + +namespace DAZ_Installer +{ + internal static class SerilogLoggerConstants + { + internal static ExpressionTemplate Template = new ExpressionTemplate("[{@t:yyyy-MM-dd HH:mm:ss} {@l:u3}] [Thread {ThreadId}]" + + "{#if SourceContext is not null} [{Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1)}]{#end}" + + //"{#each _, v in Rest()}" + " [{v}]{#end}" + + "{#if Archive is not null} [{Archive}]{#end}" + + "{#if File is not null} [{File}]{#end}" + + ": {@m}\n{@x}"); + internal static ExpressionTemplate LoggerTemplate = new ExpressionTemplate("[{@l:u3}]" + + "{#if SourceContext is not null} [{Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1)}]{#end}" + + //"{#each _, v in Rest()}" + " [{v}]{#end}" + + "{#if Archive is not null} [{Archive}]{#end}" + + "{#if File is not null} [{File}]{#end}" + + ": {@m}\n{@x}"); + + } +} diff --git a/src/Utilities/SpanExtensions.cs b/src/DAZ_Installer.Common/SpanExtensions.cs similarity index 96% rename from src/Utilities/SpanExtensions.cs rename to src/DAZ_Installer.Common/SpanExtensions.cs index bedda3b..b2d9135 100644 --- a/src/Utilities/SpanExtensions.cs +++ b/src/DAZ_Installer.Common/SpanExtensions.cs @@ -1,6 +1,6 @@ using System; -namespace DAZ_Installer.Utilities +namespace DAZ_Installer { internal static class SpanExtensions { diff --git a/src/DAZ_Installer.Common/TryHelper.cs b/src/DAZ_Installer.Common/TryHelper.cs new file mode 100644 index 0000000..5206309 --- /dev/null +++ b/src/DAZ_Installer.Common/TryHelper.cs @@ -0,0 +1,132 @@ +using DAZ_Installer.IO; +using System; +using System.IO; +using System.Diagnostics.CodeAnalysis; + +namespace DAZ_Installer +{ + /// + /// A class dedicated to try and resolve exceptions in try/catch blocks. For example, dealing with files that are hidden, this + /// class contains methods that will attempt to unhide the file and then try again. + /// + public static class TryHelper + { + /// + /// + /// + /// The file that we may not have access to. + /// Whether the application now has access to the file. + public static bool TryFixFilePermissions(IDPFileInfo info) => TryFixFilePermissions(info, out _); + /// + /// Tries to fix the file permissions of . If it fails, it will return false. + /// + /// The file that we may not have access to. + /// The exception that was thrown, if any. + /// Whether the application now has access to the file. + public static bool TryFixFilePermissions(IDPFileInfo info, [NotNullWhen(false)] out Exception? ex) + { + // If exists returns false, it means that either the file does not exist or we do not have access to it. + // We can't do anything if we don't have access to the file. But we can if we can not open, move, rename it + // due to file permissions. + ex = null; + if (!info.Exists) return false; + try + { + info.Attributes = FileAttributes.Normal; + info.OpenRead().Close(); // test that we have access. + return true; + } + catch (Exception e) + { + ex = e; + return false; + } + } + + /// + /// Tries to fix the directory permissions of . If it fails, it will return false. + /// + /// The directory that we may not have access to. + /// Whether + public static bool TryFixDirectoryPermissions(DirectoryInfo info) => TryFixDirectoryPermissions(info, out _); + /// + /// Tries to fix the directory permissions of . If it fails, it will return false. + /// + /// The directory that we may not have access to. + /// param name="ex">The exception that was thrown, if any. + /// Whether + public static bool TryFixDirectoryPermissions(DirectoryInfo info, [NotNullWhen(false)] out Exception? ex) + { + // If exists returns false, it means that either the file does not exist or we do not have access to it. + // We can't do anything if we don't have access to the file. But we can if we can not open, move, rename it + // due to file permissions. + ex = null; + if (!info.Exists) return false; + try + { + info.Attributes = FileAttributes.Normal; + info.EnumerateDirectories(); // test that it works. + return true; + } + catch (Exception e) + { + ex = e; + return false; + } + } + #region Generic Try Methods + /// + /// + /// + /// The function to execute in the try/catch block. + /// Whether the action ran without any exception thrown. + public static bool Try(Action action) => Try(action, out _); + /// + /// Runs in a try/catch block and returns whether it succeeded or not. + /// Succeeded means whether it threw an exception or not. + /// + /// The function to execute in the try/catch block. + /// The exception that was thrown; this will be null if is returned. + /// Whether the action ran without any exception thrown. + public static bool Try(Action action, [NotNullWhen(false)] out Exception? ex) + { + ex = null; + try { action(); } + catch (Exception e) + { + ex = e; + return false; + } + return true; + } + /// + /// + /// + /// he function to execute in the try/catch block. + /// The result that returned if it did not error, otherwise this will return default. + /// Whether the function ran without any exception thrown. + public static bool Try(Func func, out T? result) => Try(func, out result, out _); + /// + /// Runs in a try/catch block and returns whether it succeeded or not. + /// Succeeded means whether it threw an exception or not. + /// It also returns the result of in . + /// + /// The function to execute in the try/catch block. + /// The result that returned if it did not error, otherwise this will return default. + /// The exception that was thrown; this will be null if is returned. + /// Whether the function ran without any exception thrown. + public static bool Try(Func func, out T? result, [NotNullWhen(false)] out Exception? ex) + { + result = default; + ex = null; + try { result = func(); } + catch (Exception e) + { + ex = e; + return false; + } + return true; + } + #endregion + } +} diff --git a/src/DAZ_Installer.Core/.vscode/settings.json b/src/DAZ_Installer.Core/.vscode/settings.json new file mode 100644 index 0000000..b38f46d --- /dev/null +++ b/src/DAZ_Installer.Core/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "DAZ_Installer.Core.sln" +} \ No newline at end of file diff --git a/src/DAZ_Installer.Core/Abstractions/AbstractDestinationDeterminer.cs b/src/DAZ_Installer.Core/Abstractions/AbstractDestinationDeterminer.cs new file mode 100644 index 0000000..3cc0764 --- /dev/null +++ b/src/DAZ_Installer.Core/Abstractions/AbstractDestinationDeterminer.cs @@ -0,0 +1,24 @@ +using Serilog; + +namespace DAZ_Installer.Core +{ + /// + /// An abstract class for determining the destination of files inside of an archive. + /// + public abstract class AbstractDestinationDeterminer + { + virtual protected ILogger Logger { get; set; } = Log.Logger.ForContext(); + /// + /// Determines the files to extract inside of the and sets their to their destination based on the . + /// + /// The archive to determine files to extract and determine destinations. + /// The settings to base decisions off of. + /// A collection of s determined to be processed. + /// + public abstract HashSet DetermineDestinations(DPArchive arc, DPProcessSettings settings); + + public AbstractDestinationDeterminer() { } + + public AbstractDestinationDeterminer(ILogger logger) => Logger = logger; + } +} diff --git a/src/DAZ_Installer.Core/Abstractions/AbstractTagProvider.cs b/src/DAZ_Installer.Core/Abstractions/AbstractTagProvider.cs new file mode 100644 index 0000000..7274b7f --- /dev/null +++ b/src/DAZ_Installer.Core/Abstractions/AbstractTagProvider.cs @@ -0,0 +1,22 @@ +using Serilog; + +namespace DAZ_Installer.Core +{ + /// + /// An abstract class for determining the tags of an archive. + /// + public abstract class AbstractTagProvider + { + protected virtual ILogger Logger { get; set; } = Log.Logger.ForContext(); + /// + /// Provides the tags based on and it's contents. + /// + /// The archive to get tags from. + /// The settings provided if needed. + /// A collection of tags determined for . + public abstract HashSet GetTags(DPArchive arc, DPProcessSettings settings); + + public AbstractTagProvider() { } + public AbstractTagProvider(ILogger logger) => Logger = logger; + } +} diff --git a/src/DAZ_Installer.Core/ContentType.cs b/src/DAZ_Installer.Core/ContentType.cs new file mode 100644 index 0000000..ba4b79e --- /dev/null +++ b/src/DAZ_Installer.Core/ContentType.cs @@ -0,0 +1,45 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE +namespace DAZ_Installer.Core +{ + /// + /// ContentType represents the type of the content for user daz files (DSON user files). + /// + /// For example, type: "wearable" found in a DSON user file + /// should have a ContentType of . + /// + /// + public enum ContentType + { + Scene, + Scene_Subset, + Hierachical_Material, + Preset_Hierarchical_Pose, + Wearable, + Character, + Figure, + Prop, + Preset_Properties, + Preset_Shape, + Preset_Pose, + Preset_Material, + Preset_Shader, + Preset_Camera, + Preset_Light, + Preset_Render_Settings, + Preset_Simulation_Settings, + Preset_DFormer, + Preset_Layered_Image, + Preset_Puppeteer, + Modifier, // aka morph + UV_Set, + Script, + Library, + Program, + Media, + Document, + Geometry, + DAZ_File, + Unknown + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.Core/DAZ_Installer.Core.csproj b/src/DAZ_Installer.Core/DAZ_Installer.Core.csproj new file mode 100644 index 0000000..85875e5 --- /dev/null +++ b/src/DAZ_Installer.Core/DAZ_Installer.Core.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + enable + x64 + + + + + + + + + + + + + + + + + + + + diff --git a/src/DAZ_Installer.Core/DAZ_Installer.Core.sln b/src/DAZ_Installer.Core/DAZ_Installer.Core.sln new file mode 100644 index 0000000..d3a62c0 --- /dev/null +++ b/src/DAZ_Installer.Core/DAZ_Installer.Core.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DAZ_Installer.Core", "DAZ_Installer.Core.csproj", "{53B9B499-922B-44BE-85C9-7D8277842326}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {53B9B499-922B-44BE-85C9-7D8277842326}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53B9B499-922B-44BE-85C9-7D8277842326}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53B9B499-922B-44BE-85C9-7D8277842326}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53B9B499-922B-44BE-85C9-7D8277842326}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0D484C5E-E2C0-4DAD-9C25-6840A246968C} + EndGlobalSection +EndGlobal diff --git a/src/DAZ_Installer.Core/DPAbstractNode.cs b/src/DAZ_Installer.Core/DPAbstractNode.cs new file mode 100644 index 0000000..78211fe --- /dev/null +++ b/src/DAZ_Installer.Core/DPAbstractNode.cs @@ -0,0 +1,96 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE +using DAZ_Installer.IO; +using Serilog; +using IOPath = System.IO.Path; + +namespace DAZ_Installer.Core +{ + /// + /// Abstract class for all elements found in archives (including archives). + /// This means that all files, and archives (which are files) should extend + /// this class. + /// + public abstract class DPAbstractNode + { + public abstract ILogger Logger { get; set; } + /// + /// The file name of a file or folder. + /// + public virtual string FileName => IOPath.GetFileName(Path); + /// + /// The full path of the file (or folder) in the archive space. Using this property is not recommended + /// for comparing or listing files as delimiters vary, use instead. + /// However since this property holds the exact path given from the archive, you can use to compare + /// to match a to the archive's native format. For example, + /// + /// RARFileInfo info = e.fileInfo; + /// info.FileName == Path // returns true + /// + /// + public string Path = string.Empty; + /// + /// The path with all forward slashes replaced with backslashes. Use this property for comparing + /// and listing files, folders, and archives. + /// + public virtual string NormalizedPath => PathHelper.NormalizePath(Path); + /// + /// The extension of the file in lowercase characters and without the dot. ext can be empty. + /// + public virtual string Ext => GetExtension(Path); + /// + /// The folder the file (or folder) is a child of. Can be null. + /// + public DPFolder? Parent { get => parent; set => UpdateParent(value); } + /// + /// The archive this file is associated to. Can be null. + /// Should only be null if this file is an AND + /// the initial archive called by + /// + public DPArchive? AssociatedArchive { get; set; } + + protected abstract void UpdateParent(DPFolder? parent); + + #region Processing Properties + /// + /// The final, absolute path that the file is supposed to be extracted to. + /// + public string TargetPath { get; set; } = string.Empty; + /// + /// The full relative path of the file (or folder) relative to the determined content folder (if any). + /// If no content folder is detected, relative path will be . + /// Currently, relative path is not set for folders. + /// + public string RelativePathToContentFolder { get; set; } = string.Empty; + /// + /// The relative directory path at which will be used to determine which the file will go to in the system. + /// This property is used to determine the target path of a file. + /// The value will be equal to + /// if the is not in . + /// + public string RelativeTargetPath { get; set; } = string.Empty; + #endregion + + protected DPFolder? parent; + + /// + /// Returns the extension of a given name without the dot and lowered to all lowercase. + /// + public static string GetExtension(string path) => IOPath.GetExtension(path).Substring(path.Length > 0 ? 1 : 0).ToLower(); + /// + /// A constructor that does nothing. Only recommended for creating init-archives. + /// + public DPAbstractNode() { } + /// + /// Constructor for creating file, folder, and even archive objects from the archive space. + /// + /// Thrown when or is . + public DPAbstractNode(string _path, DPArchive? associatedArchive = null) + { + ArgumentNullException.ThrowIfNull(_path); + Path = _path; + AssociatedArchive = associatedArchive; + } + + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.Core/DPArchive.cs b/src/DAZ_Installer.Core/DPArchive.cs new file mode 100644 index 0000000..fbcc083 --- /dev/null +++ b/src/DAZ_Installer.Core/DPArchive.cs @@ -0,0 +1,495 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE +using DAZ_Installer.Core.Extraction; +using DAZ_Installer.IO; +using Serilog; +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using IOPath = System.IO.Path; + +namespace DAZ_Installer.Core +{ + /// + /// Defines the archive type of an archive. + /// + // TODO: Add a type for Multi-Product and Multi-Bundle to help determine whether + // an archive should be added to the database/library + public enum ArchiveType + { + Product, Bundle, Unknown + } + + /// + /// Defines the archive format of an archive. + /// + public enum ArchiveFormat + { + SevenZ, WinZip, RAR, Unknown + } + /// + /// Abstract class for all supported archive files. + /// Currently the supported archive files are RAR, WinZip, and 7z (partially). + /// + public class DPArchive : DPFile + { + public override ILogger Logger { get; set; } = Log.Logger.ForContext(); + public override string FileName => !IsInnerArchive ? FileInfo!.Name : IOPath.GetFileName(NormalizedPath); + public override string Ext => IsInnerArchive ? base.Ext : GetExtension(FileInfo?.Name ?? string.Empty); + /// + /// The product name of the archive. If the archive has not been successfully processed, the product name will be equivalent to . + /// Otherwise, it is either the product name of the archive determined via the manifest file, a regex-filtered file name, or simply . + /// + public virtual string ProductName => getProductName(); + /// + /// The archive format of the archive. If the archive has not been successfully processed, the archive format will be equivalent to . + /// + public ArchiveFormat ArchiveFormat { get; protected set; } = ArchiveFormat.Unknown; + /// + /// The extractor that the archive will use. This could be null if the is . But after construction of this object, it is usually not null. + /// + public DPAbstractExtractor? Extractor { get; set; } + /// + /// The file system to use, this is derived from the which is from . + /// + public AbstractFileSystem FileSystem => FileInfo!.FileSystem; + /// + /// The name that will be used for the list view. The list name is the working archive's FileName + $"\\{Path}". + /// + /// The working archive's FileName + $"\\{Path}". + public string ListName => AssociatedArchive is null ? string.Empty : AssociatedArchive.FileName + '\\' + Path; + /// + /// A list of archives that are children of this archive. + /// Or, in other words, archives that are contained within this archive. + /// + public List Subarchives { get; init; } = new(); + /// + /// A file that has been detected as a manifest file. + /// + // TODO: Make this a list of manifest files and fetch from here. + public List ManifestFiles { get; protected set; } = new(2); + /// + /// A file that has been detected as a supplement file. + /// + // TODO: Make this a list of supplement files and fetch from here. + public List SupplementFiles { get; protected set; } = new(1); + /// + /// A boolean value to describe if this archive is a child of another archive. Default is false. + /// + public bool IsInnerArchive => AssociatedArchive is not null; + /// + /// The type of this archive. Default is . + /// + public ArchiveType Type { get; set; } = ArchiveType.Unknown; + /// + /// The product info connected to this archive. + /// + public DPProductInfo ProductInfo = new(); + + /// + /// A map of all of the folders parented to this archive. + /// + /// The NormalizedPath of the Folder. + /// The folder. + + public Dictionary Folders { get; } = new(); + + /// + /// A list of folders at the root level of this archive. + /// + public List RootFolders { get; } = new(); + /// + /// A dictionary of all of the contents and their normalized paths () in the archive. + /// + /// The file content in this archive. + /// The of a file. + public Dictionary Contents { get; } = new(); + + /// + /// A list of the root contents/ the contents at root level (DPAbstractFiles) of this archive. + /// + /// The file content in this archive. + public List RootContents { get; } = new(); + /// + /// A list of all .dsx files in this archive. + /// + /// A file that is either a manifest, supplementary, or support file (.dsx). + public List DSXFiles { get; } = new(); + /// + /// A list of all readable daz files in this archive. This consists of types with extension: .duf, .dsf. + /// + /// A file with the extension .duf OR .dsf. + /// + public List DazFiles { get; } = new(); + /// + /// The true uncompressed size of the archive contents in bytes. + /// + public ulong TrueArchiveSize { get; internal set; } = 0; + /// + /// The expected tag count for this archive. This value is updated when an applicable file has discovered new tags. + /// + public uint ExpectedTagCount { get; set; } = 0; + + /// + /// The regex expression used for creating a product name. + /// + public static Regex ProductNameRegex { get; protected set; } = new(@"([^+|\-|_|\s]+)", RegexOptions.Compiled); + + public DPArchive() { } + /// + /// Create an archive that is on the disk. + /// + /// A FileInfo object that represents an archive on the file system. + /// Thrown when is . + public DPArchive(IDPFileInfo info) : base(string.Empty, null, null) + { + FileInfo = info; + // Try to determine the archive format NOW using most reliable method if possible. + ArchiveFormat = GetArchiveFormat(); + Extractor = GetDefaultExtractorForArchiveFormat(ArchiveFormat); + } + public DPArchive(string _path, DPArchive? parent = null, DPFolder? parentFolder = null) : base(_path, parent, parentFolder) + { + // Make a file but we don't want to check anything. + //if (IsInnerArchive) Parent = null; + //else base.parent = null; + RelativePathToContentFolder = FileName; + ProductInfo = new DPProductInfo(IOPath.GetFileNameWithoutExtension(Path)); + parent?.Subarchives.Add(this); + + // Try to determine the archive format NOW using most reliable method if possible. + ArchiveFormat = GetArchiveFormat(); + Extractor = GetDefaultExtractorForArchiveFormat(ArchiveFormat); + } + + /// + /// Constructor for testing purposes. + /// + internal DPArchive(string _path, ILogger logger, IDPFileInfo info, DPAbstractExtractor extractor, DPArchive? parent = null, DPFolder? parentFolder = null) : base(_path, parent, parentFolder, info, logger) + { + // Make a file but we don't want to check anything. + //if (IsInnerArchive) Parent = null; + //else base.parent = null; + RelativePathToContentFolder = FileName; + ProductInfo = new DPProductInfo(IOPath.GetFileNameWithoutExtension(Path)); + parent?.Subarchives.Add(this); + ArchiveFormat = GetArchiveFormat(); + Extractor = extractor; + } + + // This is a great use for an AI solution. + protected virtual string getProductName() + { + // If we found the product name from the manifest, then use that since it is the most reliable. + if (!string.IsNullOrWhiteSpace(ProductInfo.ProductName)) return ProductInfo.ProductName; + // otherwise, try to get the product name from the archive name. + // Get the product name from the archive file name without extension. + // Product name excludes any +, -, _, or whitespaces. + var path = IOPath.GetFileNameWithoutExtension(IsInnerArchive ? FileName : FileInfo!.Name); + var matches = ProductNameRegex.Matches(path); + if (matches.Count == 0) return path; + else return string.Join(' ', matches.Select(x => x.Value)); + } + #region Public Methods + /// + /// Creates a new archive that lives on the disk and has no parent. + /// + /// The file info to use for I/O operations. + /// A new . + public static DPArchive CreateNewParentArchive(IDPFileInfo info) => new(info); + /// + /// Peeks the archive contents if possible and will extract ALL archive contents to . + /// + /// The destination path to extract the archive contents to. + /// The temporary path to extract the archive contents to. + /// Determines whether to overwrite the files on disk if they exist. + /// or does not exist or do not have access to it. + public DPExtractionReport ExtractAllContents(string tempLocation, bool overwrite = true) => + ExtractContents(new DPExtractSettings(tempLocation, Contents.Values, overwrite)); + /// + /// Extracts contents from the archive (using ), then peeks the archive contents if possible and + /// will attempt to extract files specifed in to . + /// + /// The settings to use for extraction. + /// Archive needed to be extracted first, but it failed to be extracted. + public DPExtractionReport ExtractContents(DPExtractSettings settings) + { + if (!Extracted && !ExtractToTemp(settings)) + throw new IOException("Archive was not on disk and could not be extracted."); + if (Extractor is null) + throw new InvalidOperationException("Extractor is null. Cannot extract archive contents."); + return Extractor.Extract(settings); + } + /// + /// Extracts contents from the archive (using ), then peeks the archive contents if possible and + /// will attempt to extract files specifed in to . + /// + /// The settings to use for extraction. will be ignored. + /// Archive needed to be extracted first, but it failed to be extracted. + public DPExtractionReport ExtractContentsToTemp(DPExtractSettings settings) + { + if (!Extracted && !ExtractToTemp(settings)) + throw new IOException("Archive was not on disk and could not be extracted."); + if (Extractor is null) + throw new InvalidOperationException("Extractor is null. Cannot extract archive contents."); + return Extractor.ExtractToTemp(settings); + } + + /// + /// Previews the archive by discovering files in this archive. If the archive is not on disk, then it will be first extracted to . + /// If is null, then it will be extracted to the temp directory. + /// + /// The temp path to extract if the archive is not on disk, otherwise it will extract to + public void PeekContents(string? temp = null) + { + // Just extract to temp. + var settings = new DPExtractSettings(temp ?? IOPath.GetTempPath(), Array.Empty(), archive: this); + if (!Extracted && !ExtractToTemp(settings)) + throw new IOException("Archive was not on disk and could not be extracted."); + if (Extractor is null) + throw new InvalidOperationException("Extractor is null. Cannot peek archive contents."); + Extractor.Peek(this); + } + + /// + /// Extracts the from the archive to the file's TargetPath. If this archive needs to be extracted first, + /// then the archive will extract this archive first then extract the requested . + /// + /// The file to extract. + /// The temp path to use if needed. + /// Determines whether to overwrite the files on disk if they exist. + public bool ExtractContent(DPFile file, string tempLocation, bool overwrite = true) => ExtractContents(new DPExtractSettings(tempLocation, new DPFile[] { file }, overwrite)).SuccessPercentage == 1; + + /// + /// Checks whether or not the given ext is what is expected. Checks file headers. Does not throw exceptions. + /// + /// Returns an extension of the appropriate archive extraction method. + /// Returns on errors and when it doesn't match archive magic strings. + + public static ArchiveFormat DetermineArchiveFormatPrecise(string location) + { + try + { + using FileStream stream = File.OpenRead(location); + var bytes = new byte[8]; + stream.Read(bytes, 0, 8); + stream.Close(); + // ZIP File Header + // 50 4B OR 57 69 + if ((bytes[0] == 80 || bytes[0] == 87) && (bytes[1] == 75 || bytes[2] == 105)) + return ArchiveFormat.WinZip; + // RAR 5 consists of 8 bytes. 0x52 0x61 0x72 0x21 0x1A 0x07 0x01 0x00 + // RAR 4.x consists of 7. 0x52 0x61 0x72 0x21 0x1A 0x07 0x00 + // Rar! + if (bytes[0] == 82 && bytes[1] == 97 && bytes[2] == 114 && bytes[3] == 33) + return ArchiveFormat.RAR; + + if (bytes[0] == 55 && bytes[1] == 122 && bytes[2] == 188 && bytes[3] == 175) + return ArchiveFormat.SevenZ; + + return ArchiveFormat.Unknown; + } + catch { return ArchiveFormat.Unknown; } + } + /// + /// + /// + /// The stream to use. + /// Determines whether to close the stream when finished. + /// Returns an extension of the appropriate archive extraction method. Otherwise, null. + public static ArchiveFormat DetermineArchiveFormatPrecise(Stream stream, bool closeWhenFinished) + { + try + { + var bytes = new byte[8]; + stream.Read(bytes, 0, 8); + if (closeWhenFinished) stream.Close(); + // ZIP File Header + // 50 4B OR 57 69 + if ((bytes[0] == 80 || bytes[0] == 87) && (bytes[1] == 75 || bytes[2] == 105)) + return ArchiveFormat.WinZip; + // RAR 5 consists of 8 bytes. 0x52 0x61 0x72 0x21 0x1A 0x07 0x01 0x00 + // RAR 4.x consists of 7. 0x52 0x61 0x72 0x21 0x1A 0x07 0x00 + // Rar! + if (bytes[0] == 82 && bytes[1] == 97 && bytes[2] == 114 && bytes[3] == 33) + return ArchiveFormat.RAR; + + if (bytes[0] == 55 && bytes[1] == 122 && bytes[2] == 188 && bytes[3] == 175) + return ArchiveFormat.SevenZ; + + return ArchiveFormat.Unknown; + } + catch { return ArchiveFormat.Unknown; } + } + + /// + /// Returns an enum describing the archive's format based on the file extension. + /// This is used for determining archive files inside of an archive. + /// + /// The path of the archive. + /// A ArchiveFormat enum determining the archive format. + public static ArchiveFormat DetermineArchiveFormat(string ext) + { + // ADDITONAL NOTE: This is called for determing archive files inside of an + // archive file. + ext = ext.ToLower(); + switch (ext) + { + case "7z": + return ArchiveFormat.SevenZ; + case "rar": + return ArchiveFormat.RAR; + case "zip": + return ArchiveFormat.WinZip; + default: + if (uint.TryParse(ext, out var _)) return ArchiveFormat.SevenZ; + return ArchiveFormat.Unknown; + } + } + /// + /// Calls on the property and + /// potentially calls if is true. + /// + /// + protected ArchiveFormat GetArchiveFormat() + { + ArchiveFormat result1 = DetermineArchiveFormat(Ext); + if (!Extracted) return result1; + ArchiveFormat result2 = DetermineArchiveFormatPrecise(FileInfo!.OpenRead(), true); + return result2; + } + + /// + /// Updates the property. If is null, it will be set to the + /// extractor that matches the property. + /// If is , will be set to null. + /// If is not null, will be set to and no checks will be made. + /// + /// + public void SetExtractor(DPAbstractExtractor? extractor = null) + { + if (extractor != null) + { + Extractor = extractor; + return; + } + ArchiveFormat = GetArchiveFormat(); + if (ArchiveFormat == ArchiveFormat.Unknown) + { + Extractor = null; + return; + } + Extractor = GetDefaultExtractorForArchiveFormat(ArchiveFormat); + } + + /// + /// Returns the default extractor given an archive format. + /// + /// The format to get default extractor for. + /// The default extractor, null if is + public static DPAbstractExtractor? GetDefaultExtractorForArchiveFormat(ArchiveFormat format) + { + return format switch + { + ArchiveFormat.SevenZ => new DP7zExtractor(), + ArchiveFormat.RAR => new DPRARExtractor(), + ArchiveFormat.WinZip => new DPZipExtractor(), + _ => null, + }; + } + + /// + /// Searches for all files that contains the name specified by . + /// + /// The name to search for. + /// The first file that contains ; null if not found. + public DPFile? FindFileViaNameContains(string name) => Contents.Values.First(x => x.FileName.Contains(name)); + + private static List ConvertDPFoldersToStringArr(Dictionary folders) + { + var a = new List(folders.Count); + foreach (DPFolder v in folders.Values) a.Add(v.Path); + return a; + } + + /// + /// This function should be called after all the files have been extracted. If no content folders have been found, this is a bundle. + /// + public ArchiveType DetermineArchiveType() + { + foreach (DPFolder folder in Folders.Values) + { + if (folder.IsContentFolder) + { + return ArchiveType.Product; + } + } + foreach (DPFile content in Contents.Values) + { + if (content is DPArchive) return ArchiveType.Bundle; + } + return ArchiveType.Unknown; + + } + + public int GetEstimateTagCount() + { + var count = 0; + foreach (DPFile content in Contents.Values) + { + count += content.Tags.Count; + } + count += ProductInfo.Authors.Count; + return count; + } + + public DPFolder? FindParent(DPAbstractNode obj) + { + var fileName = PathHelper.GetFileName(obj.Path); + // This means obj.Path contains trailing seperator, so do it again but without the seperator. + if (fileName == string.Empty) fileName = IOPath.GetFileName(obj.Path.TrimEnd(PathHelper.GetSeperator(obj.Path))); + var relativePathOnly = ""; + try + { + relativePathOnly = PathHelper.CleanDirPath(obj.Path.Remove(obj.Path.LastIndexOf(fileName))); + } + catch { } + if (FindFolder(relativePathOnly, out DPFolder? folder)) + { + return folder; + } + return null; + } + + public bool FolderExists(string fPath) => Folders.ContainsKey(fPath); + /// + /// Simply finds the folder given a relative path. + /// + /// + /// + /// + public bool FindFolder(string relativePath, [NotNullWhen(true)] out DPFolder? folder) + { + var normalizedRelativePath = PathHelper.NormalizePath(relativePath); + foreach (DPFolder _folder in Folders.Values) + { + if (_folder.NormalizedPath == normalizedRelativePath) + { + folder = _folder; + return true; + } + } + folder = null; + return false; + } + + /// + /// Uses the to split the product name into tokens. + /// + /// The name of a file to split. + /// The tokens of . + public static IEnumerable RegexSplitName(string name) => + ProductNameRegex.Matches(name).Select(x => x.Value); + + #endregion + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.Core/DPArchiveEnterArgs.cs b/src/DAZ_Installer.Core/DPArchiveEnterArgs.cs new file mode 100644 index 0000000..4afb555 --- /dev/null +++ b/src/DAZ_Installer.Core/DPArchiveEnterArgs.cs @@ -0,0 +1,16 @@ +namespace DAZ_Installer.Core +{ + /// + /// Represents arguments for when the is about + /// to process an archive. + /// + public class DPArchiveEnterArgs : EventArgs + { + /// + /// The archive that is about to be processed. + /// + public readonly DPArchive Archive; + + internal DPArchiveEnterArgs(DPArchive archive) => Archive = archive; + } +} diff --git a/src/DAZ_Installer.Core/DPArchiveErrorArgs.cs b/src/DAZ_Installer.Core/DPArchiveErrorArgs.cs new file mode 100644 index 0000000..5f31826 --- /dev/null +++ b/src/DAZ_Installer.Core/DPArchiveErrorArgs.cs @@ -0,0 +1,51 @@ +namespace DAZ_Installer.Core +{ + /// + /// Represents error arguments for errors. + /// + public class DPArchiveErrorArgs : DPProcessorErrorArgs + { + /// + /// The archive the was processing when the error occurred. + /// + public DPArchive Archive { get; init; } + /// + /// Determine whether the archive should cancel the operation or not. + /// + /// Change this value to if you wish to cancel processing the + /// archive. + /// This will only be honored if is . + /// + public new bool CancelOperation { get; set; } = true; + /// + /// + /// + /// The exception thrown by the error, if any. + /// The additional explaination for the error/situation. + /// The archive that errored. + internal DPArchiveErrorArgs(DPArchive archive, Exception? ex = null, + string? explaination = null) : base(ex, explaination) + { + Ex = ex; + if (explaination != null) + Explaination = explaination; + Archive = archive; + } + /// + /// Explanation for when the archive is encrypted. + /// + internal const string EncryptedArchiveExplanation = "Cannot process encrypted archives at this time."; + /// + /// Explanation for when the archive contains encrypted files. + /// + internal const string EncryptedFilesExplanation = "Cannot process archives with encrypted files at this time."; + internal const string UnauthorizedAccessExplanation = "Failed to extract file due to unauthorized access."; + internal const string UnauthorizedAccessAfterExplanation = "Failed to extract file due to unauthorized access (even after attempting to fix file attribute)."; + internal const string ArchiveDoesNotExistOrNoAccessExplanation = "Archive does not exist on disk or has permissions issue."; + /// + /// Format for explanation for when a file is not part of the archive. + /// + + internal const string FileNotPartOfArchiveErrorFormat = "File {0} is not part of this archive."; + } +} diff --git a/src/DAZ_Installer.Core/DPArchiveExitArgs.cs b/src/DAZ_Installer.Core/DPArchiveExitArgs.cs new file mode 100644 index 0000000..f3a604d --- /dev/null +++ b/src/DAZ_Installer.Core/DPArchiveExitArgs.cs @@ -0,0 +1,30 @@ +using DAZ_Installer.Core.Extraction; +using System.Diagnostics.CodeAnalysis; + +namespace DAZ_Installer.Core +{ + public class DPArchiveExitArgs + { + /// + /// Represents whether the file had been successfully processed by or not. + /// Will be if cancelled or an error occurred preventing main operations. + /// + public readonly bool Processed = false; + + /// + /// The archive that has finished processing. + /// + public readonly DPArchive Archive; + /// + /// The extraction report generated by the archive. This will not be null if is . + /// + public readonly DPExtractionReport? Report; + + internal DPArchiveExitArgs(DPArchive archive, DPExtractionReport? report, bool processed = true) + { + Archive = archive; + Report = report; + Processed = processed; + } + } +} diff --git a/src/DP/DPContentInfo.cs b/src/DAZ_Installer.Core/DPContentInfo.cs similarity index 60% rename from src/DP/DPContentInfo.cs rename to src/DAZ_Installer.Core/DPContentInfo.cs index d53439b..b79fb38 100644 --- a/src/DP/DPContentInfo.cs +++ b/src/DAZ_Installer.Core/DPContentInfo.cs @@ -1,31 +1,32 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE -using System.Collections.Generic; -namespace DAZ_Installer.DP { - internal struct DPContentInfo { +namespace DAZ_Installer.Core +{ + public struct DPContentInfo + { /// /// The content type of the DSON User File. Default is ContentType.Unknown. /// - internal ContentType ContentType {get; set;} = ContentType.Unknown; + public ContentType ContentType { get; set; } = ContentType.Unknown; /// /// The list of authors found in the DSON user file. Default is an empty list. /// - internal List Authors = new List(); + public List Authors = new(); /// /// The website found in the DSON user file. Default is an empty string. /// - internal string Website {get; set;} = string.Empty; + public string Website { get; set; } = string.Empty; /// /// The email found in the DSON user file. Default is an empty string. /// - internal string Email {get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; /// /// The ID found in the DSX user file. Default is an empty string. /// - internal string ID {get; set; } = string.Empty; + public string ID { get; set; } = string.Empty; - public DPContentInfo() {} // microsoftttttttt (╯‵□′)╯︵┻━┻┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻ + public DPContentInfo() { } // microsoftttttttt (╯‵□′)╯︵┻━┻┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻ } } \ No newline at end of file diff --git a/src/DP/DPDSXElement.cs b/src/DAZ_Installer.Core/DPDSXElement.cs similarity index 52% rename from src/DP/DPDSXElement.cs rename to src/DAZ_Installer.Core/DPDSXElement.cs index 1afcf58..2c0c1ed 100644 --- a/src/DP/DPDSXElement.cs +++ b/src/DAZ_Installer.Core/DPDSXElement.cs @@ -1,35 +1,33 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE -using System; -using System.Collections.Generic; - -namespace DAZ_Installer.DP { - internal class DPDSXElement +namespace DAZ_Installer.Core +{ + public class DPDSXElement { - internal readonly Dictionary attributes = new Dictionary(); - internal Memory InnerText = new char[] { }; - internal string TagName = string.Empty; - internal Memory TotalMessage = new char[] { }; - internal bool IsSelfClosingElement = false; - internal bool MessageIncludesEnding = false; - internal int BeginningIndex = -1; - internal int EndIndex = -1; + public readonly Dictionary attributes = new(); + public Memory InnerText = new char[] { }; + public string TagName = string.Empty; + public Memory TotalMessage = new char[] { }; + public bool IsSelfClosingElement = false; + public bool MessageIncludesEnding = false; + public int BeginningIndex = -1; + public int EndIndex = -1; - internal DPDSXElement Parent; - internal DPDSXElement NextSibling; - internal DPDSXElement PreviousSibling; - internal List Children = new List(); - internal DPDSXElementCollection File; - internal DPDSXElement() { } + public DPDSXElement Parent; + public DPDSXElement NextSibling; + public DPDSXElement PreviousSibling; + public List Children = new(); + public DPDSXElementCollection File; + public DPDSXElement() { } /// /// DSXElements within the given index range will have its parent set to this DSXElement and added to children array. /// /// The total beginning index of the buffer array. /// The total end index of the buffer array. - internal void ParentChildrenWithinIndexRange() + public void ParentChildrenWithinIndexRange() { - var workingSibling = NextSibling; + DPDSXElement workingSibling = NextSibling; while (workingSibling != null && IndexInRange(BeginningIndex, EndIndex, workingSibling)) { workingSibling.Parent = this; @@ -45,5 +43,4 @@ protected static bool IndexInRange(int beginningIndex, int endIndex, DPDSXElemen } } -} - \ No newline at end of file +} diff --git a/src/DAZ_Installer.Core/DPDSXElementCollection.cs b/src/DAZ_Installer.Core/DPDSXElementCollection.cs new file mode 100644 index 0000000..257f24d --- /dev/null +++ b/src/DAZ_Installer.Core/DPDSXElementCollection.cs @@ -0,0 +1,50 @@ +namespace DAZ_Installer.Core +{ + public class DPDSXElementCollection + { + public Dictionary> selfClosingElements = new(3); + public Dictionary> nonSelfClosingElements = new(3); + public int Count { get; private set; } = 0; + public void AddElement(DPDSXElement element) + { + Count++; + var newLinkedList = new LinkedList(); + if (element.IsSelfClosingElement) + { + if (!selfClosingElements.TryAdd(element.TagName, newLinkedList)) + { + selfClosingElements[element.TagName].AddLast(element); + } + newLinkedList.AddFirst(element); + } + else + { + if (!nonSelfClosingElements.TryAdd(element.TagName, newLinkedList)) + { + nonSelfClosingElements[element.TagName].AddLast(element); + } + else + { + newLinkedList.AddFirst(element); + } + } + } + + + public IEnumerable GetAllElements() => selfClosingElements.Values.SelectMany(list => list) + .Concat(nonSelfClosingElements.Values.SelectMany(list => list)); + + public List FindElementViaTag(string tagName) + { + if (nonSelfClosingElements.ContainsKey(tagName)) + { + return new List(nonSelfClosingElements[tagName]); + } + if (selfClosingElements.ContainsKey(tagName)) + { + return new List(selfClosingElements[tagName]); + } + return new List(0); + } + } +} diff --git a/src/DAZ_Installer.Core/DPDSXFile.cs b/src/DAZ_Installer.Core/DPDSXFile.cs new file mode 100644 index 0000000..31eaa59 --- /dev/null +++ b/src/DAZ_Installer.Core/DPDSXFile.cs @@ -0,0 +1,116 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +using DAZ_Installer.IO; +using Serilog; + +namespace DAZ_Installer.Core +{ + /// + /// A special class that marks the DPFile as .dsx file which typically is a Supplement file or a Manifest file. + /// + public class DPDSXFile : DPFile + { + public override ILogger Logger { get; set; } = Log.Logger.ForContext(); + public bool isSupplementFile, isManifestFile = false; + public bool isSupportingFile = false; + public bool contentChecked = false; + public DPContentInfo ContentInfo = new(); + + public DPDSXFile(string _path, DPArchive arc, DPFolder? __parent) : + base(_path, arc, __parent) + { + arc.DSXFiles.Add(this); + if (FileName == "Manifest.dsx") arc.ManifestFiles.Add(this); + Tags.Clear(); // Do not include us. + } + + /// + /// Reads the contents of this file and updates the ContentInfo variables. + /// This is usually called for the Support dsx file in the Runtime/Support folder. + /// + public void CheckContents(StreamReader stream) + { + if (AssociatedArchive is null) return; + var parser = new DPDSXParser(stream); + DPDSXElementCollection collection = parser.GetDSXFile(); + List search = collection.FindElementViaTag("ProductName"); + if (search.Count != 0) + { + AssociatedArchive.ProductInfo.ProductName = search[0].attributes["VALUE"]; + } + search = collection.FindElementViaTag("Artist"); + foreach (DPDSXElement artist in search) + { + ContentInfo.Authors.Add(artist.attributes["VALUE"]); + AssociatedArchive.ProductInfo.Authors.Add(artist.attributes["VALUE"]); + } + search = collection.FindElementViaTag("ProductToken"); + if (search.Count != 0) + { + ContentInfo.ID = search[0].attributes["VALUE"]; + AssociatedArchive.ProductInfo.SKU = ContentInfo.ID; + } + contentChecked = true; + } + + /// + /// Opens the manifest file, and returns a map with the keys being the full path of the file and the value being the path without "Content\" included. + /// This is only for Daz Product since they require this manifest. + /// + /// + /// Returns a dictionary containing files to extract and their destination. + /// Key is the file path in the archive, and value is the path relative to Content folder. + /// + public Dictionary GetManifestDestinations() + { + var dict = new Dictionary(); + try + { + if (!Extracted) + { + Logger.Error("Cannot get manifest destinations for a file that has not been extracted."); + return dict; + } + if (FileInfo is null) { + Logger.Error("FileInfo is null. Cannot get manifest destinations"); + return dict; + } + if (!FileInfo.TryAndFixOpenRead(out var stream, out var ex)) + { + Logger.Error(ex, "Failed to open file for reading"); + return dict; + } + using var streamReader = new StreamReader(stream!); + var parser = new DPDSXParser(streamReader); + DPDSXElementCollection collection = parser.GetDSXFile(); + IEnumerable elements = collection.GetAllElements(); + dict.EnsureCapacity(collection.Count); + foreach (DPDSXElement element in elements) + { + if (element.attributes.ContainsKey("ACTION") && new string(element.TagName) == "File") + { + var target = element.attributes["TARGET"]; + if (target == "Content") + { + // Get value. + ReadOnlySpan filePath = element.attributes["VALUE"]; + ReadOnlySpan pathWithoutContent = filePath.Slice(7).TrimStart(PathHelper.GetSeperator(filePath)); + dict[filePath.ToString()] = pathWithoutContent.ToString(); + } + else if (target == "Application") + { + Logger.Warning("Got a target where the value was Application. This is not supported yet."); + } + } + } + } + catch (Exception ex) + { + Logger.Error(ex, "An unexpected error occurred while attempting to determine destination paths through the manifest."); + } + return dict; + + } + } +} \ No newline at end of file diff --git a/src/DP/DPDSXParser.cs b/src/DAZ_Installer.Core/DPDSXParser.cs similarity index 84% rename from src/DP/DPDSXParser.cs rename to src/DAZ_Installer.Core/DPDSXParser.cs index b504f50..b053804 100644 --- a/src/DP/DPDSXParser.cs +++ b/src/DAZ_Installer.Core/DPDSXParser.cs @@ -1,41 +1,38 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE -using System; -using System.IO; -using System.Collections.Generic; +using Serilog; using System.Text; -using System.Threading.Tasks; -namespace DAZ_Installer.DP +namespace DAZ_Installer.Core { - internal class DPDSXParser + public class DPDSXParser { - protected readonly StreamReader stream; + protected readonly StreamReader stream = null!; protected int lastIndex = 0; - protected DPDSXElementCollection workingFileContents = new DPDSXElementCollection(); + protected DPDSXElementCollection workingFileContents = new(); protected const int BUFFER_SIZE = 16384; // in chars. 32 KB. protected int chunk = 0; - internal bool hasErrored; - internal string fileName = string.Empty; - protected Task asyncTask { get; set; } = null; + public bool hasErrored; + public string fileName = string.Empty; + protected Task asyncTask { get; set; } = null!; protected bool Disposed = false; - internal DPDSXParser(string path) + public DPDSXParser(StreamReader stream) { - fileName = Path.GetFileName(fileName); try { - stream = new StreamReader(path, Encoding.UTF8, true); + this.stream = stream; asyncTask = new Task(ReadFile); asyncTask.Start(); } catch (Exception e) { - DPCommon.WriteToLog(e); + Log.Logger.ForContext().Error(e, "Failed to open file {FileName}", fileName); stream?.Dispose(); Disposed = true; hasErrored = true; } + } ~DPDSXParser() { @@ -45,16 +42,16 @@ internal DPDSXParser(string path) protected void ReadFile() { var watch = new System.Diagnostics.Stopwatch(); - int offset = 0; + var offset = 0; DPDSXElement lastElement = null; Span chars = new char[BUFFER_SIZE]; - DPCommon.WriteToLog($"Reading file {fileName}..."); + // DPCommon.WriteToLog($"Reading file {fileName}..."); watch.Start(); try { while (stream.Read(chars) != 0) { - var tmp = GetNextElement(chars, 0, out lastIndex); + DPDSXElement? tmp = GetNextElement(chars, 0, out lastIndex); if (lastElement != null && tmp != null) { workingFileContents.AddElement(tmp); @@ -70,7 +67,7 @@ protected void ReadFile() while (lastElement != null) { var nextIndex = lastIndex; - var element = GetNextElement(chars, nextIndex, out nextIndex); + DPDSXElement? element = GetNextElement(chars, nextIndex, out nextIndex); lastElement.NextSibling = element; if (element != null) { @@ -84,24 +81,26 @@ protected void ReadFile() offset = chars.Length - 1 - lastIndex; } watch.Stop(); - DPCommon.WriteToLog($"Execution Time: {watch.ElapsedMilliseconds} ms"); - foreach (var element in workingFileContents.GetAllElements()) + // DPCommon.WriteToLog($"Execution Time: {watch.ElapsedMilliseconds} ms"); + foreach (DPDSXElement element in workingFileContents.GetAllElements()) { - DPCommon.WriteToLog($"Element Tag Name: {new string(element.TagName)}"); - foreach (var attribute in element.attributes) + // DPCommon.WriteToLog($"Element Tag Name: {new string(element.TagName)}"); + foreach (KeyValuePair attribute in element.attributes) { - DPCommon.WriteToLog($"Attribute Name: {attribute.Key} | Attribute Value: {attribute.Value}"); + // DPCommon.WriteToLog($"Attribute Name: {attribute.Key} | Attribute Value: {attribute.Value}"); } } - } catch (Exception e) + } + catch (Exception e) { - DPCommon.WriteToLog($"An error occurred while attempting to read DSX file. REASON: {e}"); - } finally + // DPCommon.WriteToLog($"An error occurred while attempting to read DSX file. REASON: {e}"); + } + finally { if (!Disposed) stream?.Dispose(); Disposed = true; } - + } /// @@ -113,7 +112,7 @@ protected void ReadFile() Memory totalMessage = Memory.Empty; // includes < and > lastIndex = -1; var workingElement = new DPDSXElement(); - string attributeName = string.Empty; + var attributeName = string.Empty; var attributeValue = new StringBuilder(30); var tagName = new StringBuilder(30); var isInTagCaptureMode = true; @@ -126,7 +125,7 @@ protected void ReadFile() { if (nextLessThanIndex != -1) { - + // stringBuild.Add('<'); for (var i = nextLessThanIndex + 1; i < arr.Length; i++) { @@ -177,8 +176,9 @@ protected void ReadFile() } } } - - if (nextLessThanIndex == -1) { + + if (nextLessThanIndex == -1) + { lastIndex = -1; return null; } @@ -192,11 +192,12 @@ protected void ReadFile() { // var ourTagName = arr[(nextLessThanIndex + 2)..lastIndex]; // TODO: Check if ourTagName == workingElement.TagName - var ourTagName = arr.Slice(nextLessThanIndex + 2, lastIndex - nextLessThanIndex + 1); - var ourElements = workingFileContents.FindElementViaTag(new string(ourTagName)); - if (ourElements?.Length != 0) + ReadOnlySpan ourTagName = arr.Slice(nextLessThanIndex + 2, lastIndex - nextLessThanIndex + 1); + List ourElements = workingFileContents.FindElementViaTag(new string(ourTagName)); + if (ourElements.Count != 0) { - foreach (var element in ourElements) { + foreach (DPDSXElement element in ourElements) + { element.MessageIncludesEnding = true; element.TotalMessage = totalMessage; var closingTagBeginningIndex = GetClosingTagLessThan(element.TotalMessage.Span, element.TotalMessage.Length); @@ -214,7 +215,7 @@ protected void ReadFile() } catch (Exception e) { - DPCommon.WriteToLog(e); + // DPCommon.WriteToLog(e); } lastIndex = -1; return null; @@ -293,7 +294,7 @@ protected static int GetClosingTagLessThan(ReadOnlySpan arr, int offset) return -1; } - internal DPDSXElementCollection GetDSXFile() + public DPDSXElementCollection GetDSXFile() { asyncTask.Wait(); return workingFileContents; diff --git a/src/DP/DPDazFile.cs b/src/DAZ_Installer.Core/DPDazFile.cs similarity index 54% rename from src/DP/DPDazFile.cs rename to src/DAZ_Installer.Core/DPDazFile.cs index 28d84f6..d165a90 100644 --- a/src/DP/DPDazFile.cs +++ b/src/DAZ_Installer.Core/DPDazFile.cs @@ -1,22 +1,24 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE -using System; -using System.IO; -namespace DAZ_Installer.DP { - internal class DPDazFile : DPFile { - internal DPContentInfo ContentInfo = new DPContentInfo(); - internal DPDazFile(string _path, DPFolder? __parent) : base(_path, __parent) { - DPProcessor.workingArchive.DazFiles.Add(this); - } +using Serilog; + +namespace DAZ_Installer.Core +{ + public class DPDazFile : DPFile + { + public override ILogger Logger { get; set; } = Log.Logger.ForContext(); + public DPContentInfo ContentInfo = new(); + public DPDazFile(string _path, DPArchive arc, DPFolder? __parent) : base(_path, arc, __parent) => arc.DazFiles.Add(this); /// /// Reads and updates ContentInfo struct. /// /// The file stream to read from. - internal void ReadContents(StreamReader stream) { - var tenLines = new string[10]; - for (var i = 0; i < 10; i++) + public void ReadContents(StreamReader stream) + { + var tenLines = new string?[10]; + for (var i = 0; i < 10; i++) tenLines[i] = stream.ReadLine(); UpdateContentInfo(tenLines); } @@ -25,34 +27,33 @@ internal void ReadContents(StreamReader stream) { /// Parses through the content info and updates the ContentInfo struct. Only reads lines 1 - 8. /// /// A collection of strings. - public void UpdateContentInfo(ReadOnlySpan contents) + private void UpdateContentInfo(ReadOnlySpan contents) { // 1..9 - foreach (var line in contents.Slice(1, 8)) + foreach (var line in contents.Slice(1, 9)) { - if (string.IsNullOrEmpty(line) || string.IsNullOrWhiteSpace(line)) continue; + if (string.IsNullOrWhiteSpace(line)) continue; try { - var propertyName = GetPropertyName(line); - if (propertyName.Contains("type", StringComparison.Ordinal)) ContentInfo.ContentType = GetContentType(ParseJsonValue(line, "type"), this); - else if (propertyName.Contains("author", StringComparison.Ordinal)) ContentInfo.Authors.Add(ParseJsonValue(line, "author")); - else if (propertyName.Contains("email", StringComparison.Ordinal)) ContentInfo.Email = ParseJsonValue(line, "email"); - else if (propertyName.Contains("website", StringComparison.Ordinal)) ContentInfo.Website = ParseJsonValue(line, "website"); + ReadOnlySpan propertyName = GetPropertyName(line); + if (propertyName.Contains("type", StringComparison.Ordinal)) ContentInfo.ContentType = GetContentType(ParseJsonValue(line), this); + else if (propertyName.Contains("author", StringComparison.Ordinal)) ContentInfo.Authors.Add(ParseJsonValue(line)); + else if (propertyName.Contains("email", StringComparison.Ordinal)) ContentInfo.Email = ParseJsonValue(line); + else if (propertyName.Contains("website", StringComparison.Ordinal)) ContentInfo.Website = ParseJsonValue(line); } catch (Exception e) { - DPCommon.WriteToLog($"Failed to add metadata for file: {RelativePath} REASON: {e}"); + Logger.Error(e, "Failed to add metadata for file: {0}"); } } } - public static string ParseJsonValue(ReadOnlySpan jsonString, string propertyName) + private static string ParseJsonValue(ReadOnlySpan jsonString) { - // Substring via propertyName length + 6. var colIndex = jsonString.IndexOf(':'); if (colIndex == -1) return string.Empty; - var afterColString = jsonString.Slice(colIndex+1); + ReadOnlySpan afterColString = jsonString.Slice(colIndex + 1); var startSearchIndex = afterColString.IndexOf('"'); var lastQuoteIndex = afterColString.LastIndexOf('"'); @@ -62,13 +63,12 @@ public static string ParseJsonValue(ReadOnlySpan jsonString, string proper return afterColString.Slice(startSearchIndex + 1, lastQuoteIndex - startSearchIndex - 1).ToString(); } - // Not accurate but it's okay. - // throws error when msg is "", colonIndex returns -1 which results in out of index error. - public static ReadOnlySpan GetPropertyName(ReadOnlySpan msg) { + private static ReadOnlySpan GetPropertyName(ReadOnlySpan msg) + { var colonIndex = msg.IndexOf(':'); - var propertyName = msg.Slice(0,colonIndex-1); - propertyName.Trim('"').TrimStart(); - return propertyName; + if (colonIndex == -1) return string.Empty; + ReadOnlySpan propertyName = msg.Slice(0, colonIndex - 1); + return propertyName.Trim('"').TrimStart(); } diff --git a/src/DAZ_Installer.Core/DPDestinationDeterminer.cs b/src/DAZ_Installer.Core/DPDestinationDeterminer.cs new file mode 100644 index 0000000..8aa9b57 --- /dev/null +++ b/src/DAZ_Installer.Core/DPDestinationDeterminer.cs @@ -0,0 +1,238 @@ +using DAZ_Installer.IO; +using Serilog; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DAZ_Installer.Core +{ + public class DPDestinationDeterminer : AbstractDestinationDeterminer + { + protected override ILogger Logger { get; set; } = Log.Logger.ForContext(); + + public DPDestinationDeterminer() { } + public DPDestinationDeterminer(ILogger logger) : base(logger) { } + + public override HashSet DetermineDestinations(DPArchive arc, DPProcessSettings settings) + { + // Handle Manifest first if a combo of both. + var filesToExtract = new HashSet(arc.Contents.Count); + HashSet contentFolders = new HashSet(4); + List>? destinations = null; + if (settings.InstallOption == InstallOptions.ManifestOnly || settings.InstallOption == InstallOptions.ManifestAndAuto) + contentFolders = SetContentFoldersFromManifest(arc, out destinations); + if (settings.InstallOption == InstallOptions.ManifestAndAuto || settings.InstallOption == InstallOptions.Automatic) + DetermineContentFolders(arc, contentFolders, settings); + UpdateRelativePaths(arc, settings); + + if (settings.InstallOption != InstallOptions.ManifestOnly) + DetermineViaFileSense(arc, settings, filesToExtract); + + // Determine manifest after file sense if manifest and auto. This way the manifest can override the file sense destinations. + if (settings.InstallOption == InstallOptions.ManifestOnly || settings.InstallOption == InstallOptions.ManifestAndAuto) + DetermineFromManifests(arc, destinations, settings, filesToExtract); + return filesToExtract; + } + + private HashSet SetContentFoldersFromManifest(DPArchive arc, out List> destinations) + { + HashSet contentFolders = new HashSet(4); + destinations = new List>(arc.ManifestFiles.Count); + foreach (var manifest in arc.ManifestFiles.Where(x => x.Extracted)) + { + try + { + var dest = manifest.GetManifestDestinations(); + destinations.Add(dest); + // The first folder after the "content" folder is the content folder. + foreach (var path in dest.Values) + { + var rootDir = PathHelper.GetRootDirectory(path, true); + if (string.IsNullOrEmpty(rootDir)) continue; + var result = arc.Folders.TryGetValue(PathHelper.NormalizePath(Path.Combine("Content", rootDir)), out var folder); + if (!result) + { + Logger.Warning("Could not find content folder {0} that was defined in manifest", rootDir, arc.FileName); + continue; + } + contentFolders.Add(folder); + folder.IsContentFolder = true; + } + } catch (Exception ex) + { + Logger.Error(ex, "Failed to get manifest destinations for {0}", manifest.FileName); + } + } + return contentFolders; + } + + // TODO: Update this function to determine content folders from the manifest. + // Or make this exclusive to auto mode. + private void DetermineContentFolders(DPArchive arc, HashSet ignoreSet, in DPProcessSettings settings) + { + // A content folder is a folder whose name is contained in the user's common content folders list + // or in their folder redirects map. + + + // Prepare sort so that the first elements in folders are the ones at root. + DPFolder[] folders = arc.Folders.Values.ToArray(); + var foldersKeys = new byte[folders.Length]; + + for (var i = 0; i < foldersKeys.Length; i++) + { + foldersKeys[i] = PathHelper.GetSubfoldersCount(folders[i].Path); + } + + // Elements at the beginning are folders at root levels. + Array.Sort(foldersKeys, folders); + + foreach (DPFolder? folder in folders) + { + if (ignoreSet.Contains(folder)) continue; + var folderName = Path.GetFileName(folder.Path); + var elgibleForContentFolderStatus = settings.ContentFolders.Contains(folderName) || + settings.ContentRedirectFolders.ContainsKey(folderName); + if (folder.Parent is null) + folder.IsContentFolder = elgibleForContentFolderStatus; + else + { + if (folder.Parent.IsContentFolder || folder.Parent.IsPartOfContentFolder) continue; + folder.IsContentFolder = elgibleForContentFolderStatus; + } + } + } + /// + /// Updates the relative paths of all files and folders in the archive relative to the content folder. + /// + /// The archive to check. + /// The settings to use. + private void UpdateRelativePaths(DPArchive arc, in DPProcessSettings settings) + { + foreach (DPFile content in arc.RootContents) + content.RelativePathToContentFolder = content.RelativeTargetPath = content.Path; + foreach (DPFolder folder in arc.Folders.Values) + folder.UpdateChildrenRelativePaths(settings); + } + + /// + /// This function returns the target path based on whether it is saving to it's destination or to a + /// temporary location, whether the has a relative path or not, and whether + /// the file's parent is in folderRedirects. + /// Additionally, there is which will be used for combining paths publicly; + /// however, this will be ignored if the parent name is in the user's folder redirects. + /// + /// The file to get a target path for. + /// The process settings to use. + /// Determines whether to get a target path saving to a temporary location. + /// The path to combine with instead of usual combining. + /// The target path for the specified file. + private string GetTargetPath(DPFile file, in DPProcessSettings settings, bool saveToTemp = false, string? overridePath = null) + { + var tmpLocation = Path.Combine(settings.TempPath, "DazProductInstaller"); + // file.RelativeTargetPath already substituted the content folder name with the redirect folder name. + var filePathPart = !string.IsNullOrEmpty(overridePath) ? overridePath : file.RelativeTargetPath; + + // This is the case for files determined by file sense but are not in a content folder + // (aka archives, since they are always extracted but usually never in a content folder). + // This would result in overridePath and RelativeTargetPath being empty. + if (string.IsNullOrEmpty(filePathPart)) filePathPart = file.FileName; + + if (file.Parent is null) + return Path.Combine(saveToTemp ? tmpLocation : settings.DestinationPath, filePathPart); + + var contentFolderName = GetContentFolderManifestString(file.Path); + var hasRedirect = settings.ContentRedirectFolders!.ContainsKey(contentFolderName); + // We need to + if (overridePath is not null && hasRedirect) + return Path.Combine(settings.DestinationPath, settings.ContentRedirectFolders[contentFolderName], filePathPart.Remove(0, contentFolderName.Length + 1)); + + // If the installation method includes Automatic, then relative target path calculations have been done prior to this function. + // Therefore, we can just use the relative target path. + return Path.Combine(saveToTemp ? tmpLocation : settings.DestinationPath, + filePathPart ?? file.Parent.CalculateChildRelativeTargetPath(file, settings)); + } + + private void DetermineFromManifests(DPArchive arc, List>? dests, in DPProcessSettings settings, HashSet filesToExtract) + { + if (dests is null) dests = arc!.ManifestFiles.Select(f => f.GetManifestDestinations()).ToList(); + if (settings.InstallOption != InstallOptions.ManifestAndAuto && settings.InstallOption != InstallOptions.ManifestOnly) return; + foreach (var manifestDestinations in dests) + { + foreach (DPFile file in arc.Contents.Values) + { + try + { + if (!manifestDestinations.ContainsKey(file.Path) || filesToExtract.Contains(file)) continue; + file.TargetPath = GetTargetPath(file, settings, overridePath: manifestDestinations[file.Path]); + filesToExtract.Add(file); + } + catch (Exception ex) + { + Logger.Error("Failed to determine file to extract: {0}", file.Path); + Logger.Debug("File information: {@0}", file); + } + } + } + } + + private void DetermineViaFileSense(DPArchive arc, in DPProcessSettings settings, HashSet filesToExtract) + { + + if (settings.InstallOption != InstallOptions.Automatic && settings.InstallOption != InstallOptions.ManifestAndAuto) return; + // Get contents where file was not extracted. + Dictionary.ValueCollection folders = arc.Folders.Values; + + foreach (DPFolder folder in folders) + { + if (!folder.IsContentFolder && !folder.IsPartOfContentFolder) continue; + // Update children's relative path. + folder.UpdateChildrenRelativePaths(settings); + + foreach (DPFile child in folder.Contents) + { + //Get destination path and update child destination path. + child.TargetPath = GetTargetPath(child, settings); + + filesToExtract.Add(child); + } + } + // Now hunt down all files in folders that aren't in content folders. + foreach (DPFolder folder in folders) + { + if (folder.IsContentFolder) continue; + // Add all archives to the inner archives to process for later processing. + foreach (DPFile file in folder.Contents) + { + if (file is not DPArchive nestedArc) continue; + nestedArc.TargetPath = GetTargetPath(nestedArc, settings, true); + // Add to queue. + filesToExtract.Add(nestedArc); + } + } + + // Hunt down all files in root content. + + foreach (DPFile content in arc.RootContents) + { + if (content is not DPArchive nestedArc) continue; + nestedArc.TargetPath = GetTargetPath(nestedArc, settings, true); + // Add to queue. + filesToExtract.Add(nestedArc); + } + } + + /// + /// A function that determines the content folder of the archive based off of the manifest. + /// It returns the folder name after the "Content/" folder in the manifest in the . + /// + /// The path that the manifest has for a file. + /// The content folder of the archive. + private string GetContentFolderManifestString(string path) + { + var segments = path.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); + return segments.Length > 1 ? segments[1] : string.Empty; + } + } +} diff --git a/src/DAZ_Installer.Core/DPErrorArgs.cs b/src/DAZ_Installer.Core/DPErrorArgs.cs new file mode 100644 index 0000000..194d615 --- /dev/null +++ b/src/DAZ_Installer.Core/DPErrorArgs.cs @@ -0,0 +1,17 @@ +namespace DAZ_Installer.Core +{ + /// + /// Represents an exception and a file or + /// + public class DPErrorArgs : EventArgs + { + public readonly DPAbstractNode File; + public readonly Exception Ex; + + internal DPErrorArgs(DPAbstractNode file, Exception ex) : base() + { + File = file; + Ex = ex; + } + } +} diff --git a/src/DAZ_Installer.Core/DPEventHandler.cs b/src/DAZ_Installer.Core/DPEventHandler.cs new file mode 100644 index 0000000..9ce1660 --- /dev/null +++ b/src/DAZ_Installer.Core/DPEventHandler.cs @@ -0,0 +1,8 @@ +namespace DAZ_Installer.Core +{ + public delegate void DPProcessorEventHandler(DPProcessor sender, T args); + public delegate void DPArchiveEventHandler(DPArchive archive, T args); + public delegate void DPProcessorEventHandler(DPProcessor sender); + public delegate void DPArchiveEventHandler(DPArchive archive); + +} diff --git a/src/DAZ_Installer.Core/DPFile.cs b/src/DAZ_Installer.Core/DPFile.cs new file mode 100644 index 0000000..4ff21cf --- /dev/null +++ b/src/DAZ_Installer.Core/DPFile.cs @@ -0,0 +1,254 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +using DAZ_Installer.Core.Extraction; +using DAZ_Installer.IO; +using Serilog; +using IOPath = System.IO.Path; +namespace DAZ_Installer.Core +{ + /// + /// A is a regular file that can be found in a or a . + /// + public class DPFile : DPAbstractNode + { + // Public static members + private static Dictionary enumPairs { get; } = new Dictionary(Enum.GetValues(typeof(ContentType)).Length); + public static readonly HashSet DAZFormats = new() { "duf", "dsa", "dse", "daz", "dsf", "dsb", "dson", "ds", "dsb", "djl", "dsx", "dsi", "dcb", "dbm", "dbc", "dbl", "dso", "dsd", "dsv" }; + public static readonly HashSet GeometryFormats = new() { "dae", "bvh", "fbx", "obj", "dso", "abc", "mdd", "mi", "u3d" }; + public static readonly HashSet MediaFormats = new() { "png", "jpg", "hdr", "hdri", "bmp", "gif", "webp", "eps", "raw", "tiff", "tif", "psd", "xcf", "jpeg", "cr2", "svg", "apng", "avif" }; + public static readonly HashSet DocumentFormats = new() { "txt", "pdf", "doc", "docx", "odt", "html", "ppt", "pptx", "xlsx", "xlsm", "xlsb", "rtf" }; + public static readonly HashSet OtherFormats = new() { "exe", "lib", "dll", "bat", "cmd" }; + public static readonly HashSet AcceptableImportFormats = new() { "rar", "zip", "7z", "001" }; + public List Tags { get; set; } = new List(0); + /// + /// The FileInfo object to use for moving, copying, and deleting files. Typically this is a . + /// + public IDPFileInfo? FileInfo { get; set; } + /// + /// The logger to use; typically this is of type . If you override this, make sure to use . + /// + public override ILogger Logger { get; set; } = Log.Logger.ForContext(); + #region Extraction Properties + // Properties that are generally used for extraction. + + /// + /// Determines whether the file has been extracted or not; this simply checks if is null or not.
+ /// This is because the extractor will not set until the file has been extracted.
This does not necessarily + /// mean that the file has been extracted to the target path (eg. extracted to temp first).
+ /// If you wish to check if the file has been extracted to the target path, + /// use . + ///
+ /// + public bool Extracted => FileInfo != null; + /// + /// Determines whether the file has been extracted to the target path or not. + /// + public bool ExtractedToTarget => FileInfo != null && !string.IsNullOrEmpty(TargetPath) && FileInfo.Path == IOPath.GetFullPath(TargetPath); + /// + /// The FileInfo object that represents the file that is on disk. This should only be set during initialization + /// or by the extractor. + /// + #endregion + + // TO DO : Add get tags func. + // TO DO: Add static function to search for a property. + static DPFile() + { + foreach (var eName in Enum.GetNames(typeof(ContentType))) + { + var lowercasedName = eName.ToLower(); + enumPairs[lowercasedName] = (ContentType)Enum.Parse(typeof(ContentType), eName); + } + } + /// + /// A constructor that does nothing. + /// + public DPFile() { } + + public DPFile(string _path, DPArchive? arc, DPFolder? __parent) : base(_path, arc) + { + ArgumentNullException.ThrowIfNull(_path, nameof(_path)); + if (GetType() == typeof(DPFile)) Logger.Debug("Creating new DPFile for {0}", Path); + Parent = __parent; + InitializeTagsList(); + + if (arc is null) return; + AssociatedArchive = arc; + if (!arc.Contents.TryAdd(NormalizedPath, this)) + throw new Exception("File already exists in this archive."); + } + + public DPFile(string _path, DPArchive? arc, DPFolder? __parent, IDPFileInfo? fileInfo, ILogger logger) : this(_path, arc, __parent) + { + FileInfo = fileInfo; + Logger = logger; + } + + public static DPFile CreateNewFile(string path, DPArchive? arc, DPFolder? parent) + { + var ext = GetExtension(path); + if (ext == "dsf" || ext == "duf") + { + return new DPDazFile(path, arc!, parent); + } + else if (ext == "dsx") + { + return new DPDSXFile(path, arc!, parent); + } + else if (AcceptableImportFormats.Contains(ext)) + return new DPArchive(path, arc, parent); + return new DPFile(path, arc, parent); + } + + /// + /// Attempts to move the file to the given path. This simply calls to move + /// the file in file system space (not in archive space). Throws exceptions. + /// + /// The path to move to (must exist and have access to it). + public void MoveTo(string path) => FileInfo?.MoveTo(path, true); + /// + /// Attempts to delete the file in file system space (not archive space). This simply calls to delete the file. Throws exceptions. + /// + public void Delete() => FileInfo?.Delete(); + + /// + /// Updates the parent of the file (or archive). + /// + /// The folder that will be the new parent of the file (or archive). + protected override void UpdateParent(DPFolder? newParent) + { + // If we were null, but now we're not... + if (parent == null && newParent != null) + { + // Remove ourselves from root content of the working archive. + // AssociatedArchive shouldn't be null at the point. + AssociatedArchive!.RootContents.Remove(this); + + // Call the folder's addChild() to add ourselves to the children list. + newParent.AddChild(this); + parent = newParent; + } + else if (parent == null && newParent == null) + { + // If associated archive is null, then there are no parents to look for. + // This should only happen when the file is an archive to be processed/extracted. + // Any other DPFile should have an associated archive. + if (AssociatedArchive is null) + { + parent = null; + return; + } + // Try to find a parent. + DPFolder? potParent = AssociatedArchive.FindParent(this); + + // If we found a parent, then update it. This function will be called again. + if (potParent != null) Parent = potParent; + else + { + // Create a folder for us. + potParent = DPFolder.CreateFoldersForFile(Path, AssociatedArchive); + + // If we have successfully created a folder for us, then update it. This function will be called again. + if (potParent != null) Parent = potParent; + else // Otherwise, we are supposed to be at root. + { + parent = null; + if (!AssociatedArchive.RootContents.Contains(this)) + AssociatedArchive.RootContents.Add(this); + } + } + } + else if (parent != null && newParent != null) + { + // Remove ourselves from previous parent children. + parent.RemoveChild(this); + + // Add ourselves to new parent's children. + newParent.AddChild(this); + + parent = newParent; + } + else if (parent != null && newParent == null) + { + // Remove ourselves from previous parent's children. + parent.RemoveChild(this); + + // Add ourselves to the archive's root content list. + // AssoiciatedArchive should never be null at this point. + AssociatedArchive!.RootContents.Add(this); + parent = newParent; + } + } + + /// + /// Extracts the current file to . If the is not on disk, + /// then it will be extracted first. + /// + /// The extract settings to use. + /// Whether the extraction was successful or not. + public bool Extract(DPExtractSettings settings) + { + if (AssociatedArchive is null) return false; + return AssociatedArchive!.ExtractContent(this, settings.TempPath, settings.OverwriteFiles); + } + + /// + /// Extracts the current file to by setting to and extracting. + /// + /// The extract settings to use. + /// Whether the extraction was successful or not. + /// + public bool Extract(DPExtractSettings settings, string dest) + { + TargetPath = dest; + return Extract(settings); + } + + /// + /// Extracts the current file. If the file is not extracted, then it will be extracted. Otherwise, nothing will happen. + /// + /// The extract settings to use; only will be honored. + /// + public bool ExtractToTemp(DPExtractSettings settings) + { + if (AssociatedArchive is null) return false; + return AssociatedArchive.ExtractContentsToTemp(new DPExtractSettings(settings.TempPath, new[] { this }, archive: AssociatedArchive)).SuccessPercentage == 1; + } + + public static ContentType GetContentType(string type, DPFile dP) + { + if (!string.IsNullOrEmpty(type) && enumPairs.TryGetValue(type, out ContentType contentType)) + return contentType; + if (dP is null) return ContentType.DAZ_File; + if (GeometryFormats.Contains(dP.Ext)) + return ContentType.Geometry; + else if (MediaFormats.Contains(dP.Ext)) + return ContentType.Media; + else if (DocumentFormats.Contains(dP.Ext)) + return ContentType.Document; + else if (OtherFormats.Contains(dP.Ext)) + return ContentType.Program; + else if (DAZFormats.Contains(dP.Ext)) + return ContentType.DAZ_File; + + // The most obvious comment ever - implied else :\ + return ContentType.Unknown; + } + + public static bool ValidImportExtension(string ext) => AcceptableImportFormats.Contains(ext); + + /// + /// Adds the file name to the tags name. + /// + protected void InitializeTagsList() + { + var fileName = IOPath.GetFileName(Path); + var tokens = fileName.Split(' '); + Tags = new List(tokens.Length); + Tags.AddRange(tokens); + } + + } + +} diff --git a/src/DAZ_Installer.Core/DPFolder.cs b/src/DAZ_Installer.Core/DPFolder.cs new file mode 100644 index 0000000..b9ec8ea --- /dev/null +++ b/src/DAZ_Installer.Core/DPFolder.cs @@ -0,0 +1,258 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +using DAZ_Installer.IO; +using Serilog; +using IOPath = System.IO.Path; +namespace DAZ_Installer.Core +{ + public class DPFolder : DPAbstractNode + { + public override ILogger Logger { get; set; } = Log.Logger.ForContext(); + public override string NormalizedPath => PathHelper.NormalizePath(Path); + public List subfolders = new(); + private readonly Dictionary contents = new(); + public ICollection Contents => contents.Values; + public bool IsContentFolder { get; set; } + /// + /// Determined later in ProcessArchive(). + /// + public bool IsPartOfContentFolder => (Parent?.IsPartOfContentFolder ?? false) || (Parent?.IsContentFolder ?? false); + public DPFolder(string path, DPArchive arc, DPFolder? parent) : base(path, arc) + { + Logger.Debug("Creating folder for {Path}", path); + // ZipArchive returns folders with a trailing slash, so we need to remove it. + // Potentially others may do the same. + Path = PathHelper.CleanDirPath(path); + + //if (relativePathBase != null) + //{ + // relativePath = Path.GetRelativePath(path, relativePathBase); + //} + Parent = parent; + arc.Folders.TryAdd(Path, this); + + } + + /// + /// Create folder (and subfolders) for file. This is used when a file is added to the archive and the folder it is in does not exist. + /// This can occur when certain extractors discover files first rather than folders. + /// + /// + /// + /// + public static DPFolder CreateFoldersForFile(string dpFilePath, DPArchive associatedArchive) + { + var workingStr = PathHelper.Up(dpFilePath); + DPFolder firstFolder = null; + DPFolder previousFolder = null; + + // Continously get relative path. + while (workingStr != "") + { + if (associatedArchive.FindFolder(workingStr, out _)) + { + workingStr = PathHelper.Up(workingStr); + continue; + } + if (firstFolder == null) + { + firstFolder = new DPFolder(workingStr, associatedArchive, null); + previousFolder = firstFolder; + } + else + { + var workingParent = new DPFolder(workingStr, associatedArchive, previousFolder); + previousFolder = workingParent; + } + workingStr = PathHelper.Up(workingStr); + } + return firstFolder!; + } + + public void UpdateChildrenRelativePaths(DPProcessSettings settings) + { + DPFolder? contentFolder = IsContentFolder ? this : GetContentFolder(); + if (contentFolder is null) + { + Logger.Warning("Content folder was null, could not update relative paths for {Path}", Path); + return; + } + foreach (DPFile child in contents.Values) + { + // This prevents the code for running twice on a child that was previously processed when ManifestAndAuto is on. + if (!string.IsNullOrEmpty(child.RelativePathToContentFolder) && !string.IsNullOrEmpty(child.RelativeTargetPath)) + continue; + child.RelativePathToContentFolder = contentFolder.CalculateChildRelativePath(child); + child.RelativeTargetPath = contentFolder.CalculateChildRelativeTargetPath(child, settings); + } + } + + /// + /// Calculates the path of a child relative to this folder. + /// + /// The child of this folder. + /// A string representing the relative path of the child relative to this folder. + public string CalculateChildRelativePath(DPAbstractNode child) => PathHelper.GetRelativePathOfRelativeParent(child.Path, Path); + + /// + /// Calculates the target path of a child relative to this folder. Requires the settings object to + /// check if the folder name is in . + /// + /// The child of this folder. + /// The settings object in use. + /// A string representing the target path of the child relative to this folder. + public string CalculateChildRelativeTargetPath(DPAbstractNode child, DPProcessSettings settings) + { + // TODO: In Processor, make sure the ContentRedirectFolders is never null. + var containsKey = settings.ContentRedirectFolders.ContainsKey(FileName); + if (!IsContentFolder || !containsKey) return child.RelativePathToContentFolder!; + + var i = Path.LastIndexOf(PathHelper.GetSeperator(Path)); + var newPath = PathHelper.NormalizePath( + i != -1 ? string.Concat(Path.AsSpan(0, i + 1), settings.ContentRedirectFolders[FileName]) : settings.ContentRedirectFolders[FileName] + ); + var childNewPath = PathHelper.NormalizePath(child.Path); + i = childNewPath.IndexOf(Path); + if (i != -1) childNewPath = childNewPath.Remove(i, Path.Length).Insert(i, newPath); + + return PathHelper.GetRelativePathOfRelativeParent(childNewPath, newPath); + } + + public DPFolder? GetContentFolder() + { + if (Parent == null) return null; + DPFolder? workingFolder = this; + while (workingFolder != null && workingFolder.IsContentFolder == false) + { + workingFolder = workingFolder.Parent; + } + return workingFolder; + } + + /// + /// Handles the addition of the file to children property and subfolders property (if child is a DPFolder). + /// + /// DPFolder, DPArchive, DPFile + public void AddChild(DPAbstractNode child) + { + if (child is DPFolder folder) + { + subfolders.Add(folder); + return; + } + contents.TryAdd(child.Path, (DPFile)child); + } + + public void RemoveChild(DPAbstractNode child) + { + if (child.GetType() == typeof(DPFolder)) + { + var dpFolder = (DPFolder)child; + subfolders.Remove(dpFolder); + return; + } + contents.Remove(child.Path); + } + + public DPFolder? FindFolder(string _path) + { + if (Path == _path) return this; + else + { + foreach (DPFolder folder in subfolders) + { + DPFolder? result = folder.FindFolder(_path); + if (result != null) return result; + } + } + return null; + } + public static DPFolder[] FindChildFolders(string _path, DPFolder self) + { + var folderArr = new List(); + foreach (DPFolder folder in self.AssociatedArchive!.Folders.Values) + { + if (folder == self) continue; + // And make sure it only is one level up. + if (folder.Path.Contains(_path) && IOPath.GetFileName(_path) == IOPath.GetFileName(folder.Path) + && PathHelper.GetNumOfLevelsAbove(folder.Path, _path) == 1) + { + folderArr.Add(folder); + } + } + return folderArr.ToArray(); + } + /// + /// + ///

This function removes and updates the root folders list instead of root contents list.

+ ///
+ /// The new parent for this folder. + + protected override void UpdateParent(DPFolder? newParent) + { + // If we were null, but now we're not... + if (parent == null && newParent != null) + { + // Remove ourselves from root folders list of the working archive. + try + { + AssociatedArchive!.RootFolders.Remove(this); + } + catch { } + + // Call the folder's addChild() to add ourselves to the children list. + newParent.AddChild(this); + parent = newParent; + } + else if (parent == null && newParent == null) + { + // Try to find a parent. + DPFolder? potParent = AssociatedArchive!.FindParent(this); + + // If we found a parent, then update it. This function will be called again. + if (potParent != null) + { + Parent = potParent; + } + else + { + // Otherwise, create a folder for us. + potParent = CreateFoldersForFile(Path, AssociatedArchive); + + // If we have successfully created a folder for us, then update it. This function will be called again. + if (potParent != null) Parent = potParent; + else + { // Otherwise, we are supposed to be at root. + parent = null; + if (!AssociatedArchive!.RootFolders.Contains(this)) + { + AssociatedArchive!.RootFolders.Add(this); + } + } + } + } + else if (parent != null && newParent != null) + { + // Remove ourselves from previous parent children. + parent.RemoveChild(this); + + // Add ourselves to new parent's children. + newParent.AddChild(this); + + parent = newParent; + } + else if (parent != null && newParent == null) + { + // Remove ourselves from previous parent's children. + parent.RemoveChild(this); + + // Add ourselves to the archive's root content list. + AssociatedArchive!.RootFolders.Add(this); + parent = newParent; + } + } + + + } +} diff --git a/src/DAZ_Installer.Core/DPProcessSettings.cs b/src/DAZ_Installer.Core/DPProcessSettings.cs new file mode 100644 index 0000000..4cbf2d5 --- /dev/null +++ b/src/DAZ_Installer.Core/DPProcessSettings.cs @@ -0,0 +1,85 @@ +namespace DAZ_Installer.Core +{ + /// + /// Options for how should determine which files + /// should be moved into the user's library. + /// + // TODO: Turn this into a flag, change "Automatic" to "File Sense" + // TODO: Flag 1 - Manifest, 2 - Auto, 3 - FallbackToAuto + public enum InstallOptions + { + /// + /// Will strictly read the manifest in the archive, if any, to determine + /// which files should be moved into the user's library. + /// If there is no manifest detected, then no files will be moved. + /// + ManifestOnly, + /// + /// Will rely on the manifest first then do "File Sense" by checking which files + /// are under a "Content Folder" defined in . + /// Additionally, any nested archive will be processed to find any potential products. Lastly, any files/folders + /// not under a content folder will not be moved unless it is defined in the manifest. + /// + ManifestAndAuto, + /// + /// Does "File Sense" by checking which files are under a "Content Folder" + /// defined in defined in . + /// Additionally, any nested archive will be processed to find any potential products. + /// Lastly, any files/folders not under a content folder will not be moved + /// unless it is defined in the manifest. + /// + Automatic + } + + /// + /// DPProcessSettings is used for extraction and processing operations. + /// + public struct DPProcessSettings + { + /// + /// The temporary path to use for operations. Null not allowed. + /// + public string TempPath = string.Empty; + /// + /// The final destination path to use for operations. Null not allowed. + /// + public string DestinationPath = string.Empty; + /// + /// The user-defined folder names to use as content folders. + /// If null, will use most common content folders. + /// + public HashSet? ContentFolders = null; + /// + /// The user-defined content redirect folders to redirect certain folder names to a content folder. + /// If , will use most common content redirect folders. + /// + public Dictionary? ContentRedirectFolders = null; + /// + /// The method the processor should use for determining which files should be moved into + /// the destination path or not. + /// + public InstallOptions InstallOption = InstallOptions.ManifestAndAuto; + /// + /// Determines whether the processor should overwrite files if it already exists in the user library. + /// Temp files will always be overwritten. + /// + public bool OverwriteFiles = true; + /// + /// Overrides the usual process of determining the destination of files and forces them to be moved to the specified destination in this dictionary. + /// This means that no matter what the file is, it will be moved to the specified destination. + /// + /// The file to force to a destination. + /// The destination to force the file to. + public Dictionary ForceFileToDest = new(0); + public DPProcessSettings(string temp, string dest, InstallOptions options, + HashSet? folders = null, Dictionary? redirects = null, bool overwriteFiles = true) + { + TempPath = temp; + DestinationPath = dest; + InstallOption = options; + ContentFolders = folders; + ContentRedirectFolders = redirects; + OverwriteFiles = overwriteFiles; + } + } +} diff --git a/src/DAZ_Installer.Core/DPProcessor.cs b/src/DAZ_Installer.Core/DPProcessor.cs new file mode 100644 index 0000000..ca87963 --- /dev/null +++ b/src/DAZ_Installer.Core/DPProcessor.cs @@ -0,0 +1,453 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +using DAZ_Installer.Core.Extraction; + +using DAZ_Installer.IO; +using Serilog; +using Serilog.Context; +using System.Collections.Immutable; +using System.IO.Compression; +using System.Text; +using System.Diagnostics.CodeAnalysis; + +namespace DAZ_Installer.Core +{ + // GOAL: Extract files through RAR. While it discovers files, add it to list. + // Then, deeply analyze each file; determine best approach; and execute best approach (or ask). + // Lastly, clean up. + public class DPProcessor + { + // SecureString - System.Security + // We use these variables in case the user changes the settings in mist of an extraction process + public static readonly ImmutableDictionary DefaultRedirects = ImmutableDictionary.Create(StringComparer.OrdinalIgnoreCase) + .AddRange(new KeyValuePair[]{ new("docs", "Documentation"), + new("Documents", "Documentation"), + new("Readme", "Documentation"), + new("ReadMe's", "Documentation"), + new("Readmes", "Documentation"), + new("Transport", "Vehicles"), + new("Scene", "Scenes")}); + public static readonly ImmutableHashSet DefaultContentFolders = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase) + .Union(new[] {"aniBlocks", "Animals", "Architecture", "Camera Presets", "data", "DAZ Studio Tutorials", "Documentation", "Documents", + "Environments", "General", "Light Presets", "Lights", "People", "Presets", "Props", "Render Presets", "Render Settings", "Runtime", + "Scene Builder", "Scene Subsets", "Scenes", "Scripts", "Shader Presets", "Shaders", "Support", "Templates", "Textures", "Vehicles" }); + public DPProcessSettings CurrentProcessSettings { get; private set; } = new(); + public ILogger Logger { get; set; } = Log.Logger.ForContext(); + public AbstractFileSystem FileSystem { get; set; } = new DPFileSystem(); + public AbstractTagProvider TagProvider { get; set; } = new DPTagProvider(); + public AbstractDestinationDeterminer DestinationDeterminer { get; set; } = new DPDestinationDeterminer(); + private CancellationTokenSource CancellationTokenSource { get; set; } = new(); + public CancellationToken CancellationToken { get; set; } = CancellationToken.None; + private CancellationTokenSource ArchiveCancellationSource { get; set; } = new(); + private CancellationToken ArchiveCancellationToken { get; set; } = CancellationToken.None; + private bool ArchiveCancelled => ArchiveCancellationToken.IsCancellationRequested || CancellationToken.IsCancellationRequested; + public string TempLocation => Path.Combine(CurrentProcessSettings.TempPath, @"DazProductInstaller\"); + public string DestinationPath => CurrentProcessSettings.DestinationPath; + public DPArchive CurrentArchive { get; private set; } = null!; + public ProcessorState State { get => state; private set { state = value; StateChanged?.Invoke(); } } + + /// + /// An event that is invoked when a file that is being extracted, moved, or deleted throws an error. + /// + /// + public event DPProcessorEventHandler? FileError; + /// + /// An event that is invoked when a + /// + public event DPProcessorEventHandler? ProcessError; + public event DPProcessorEventHandler? ArchiveEnter; + public event DPProcessorEventHandler? ArchiveExit; + public event DPProcessorEventHandler? ExtractProgress; + public event DPProcessorEventHandler? MoveProgress; + public event Action? Finished; + public event Action? StateChanged; + + private ProcessorState state; + // public event FilePreMove + + /// + /// Emits the event and passes the arguments required. + /// References to determine whether should + /// cancel operations or not. + /// + private void EmitOnArchiveEnter() + { + Logger.Information("Entering archive {arc}", CurrentArchive.FileName); + if (ArchiveEnter is null) return; + var args = new DPArchiveEnterArgs(CurrentArchive); + ArchiveEnter.Invoke(this, args); + } + /// + /// Emits the event and passes the arguments required. + /// + /// Tell whether the archive had been successfully processed. + private void EmitOnArchiveExit(bool successfullyProcessed, DPExtractionReport? report) + { + if (successfullyProcessed) Logger.Information("Exiting archive {0} with success", CurrentArchive.FileName); + else Logger.Warning("Exiting archive {0} with failures", CurrentArchive.FileName); + Logger.Debug("Archive exit report: {@0}", report); + ArchiveExit?.Invoke(this, new DPArchiveExitArgs(CurrentArchive, report, successfullyProcessed)); + } + + private void EmitOnProcessError(DPProcessorErrorArgs args) + { + Logger.Error(args.Ex, args.Explaination); + if (ProcessError is null) return; + ProcessError.Invoke(this, args); + } + + private void EmitOnExtractionProgress(DPArchive _, DPExtractProgressArgs args) => ExtractProgress?.Invoke(this, args); + + private void processArchiveInternal([NotNull] DPArchive archiveFile, DPProcessSettings settings) + { + Stack archivesToProcess = new(); + Stack> parentArchives = new(); + + archivesToProcess.Push(archiveFile); + CancellationTokenSource = new(); + CancellationToken = CancellationTokenSource.Token; + while (archivesToProcess.TryPop(out DPArchive arc)) + { + CurrentArchive = arc!; + DPExtractionReport? report = null; + PopParentArchive(parentArchives); + ArchiveCancellationSource = new(); + ArchiveCancellationToken = ArchiveCancellationSource.Token; + EmitOnArchiveEnter(); + try + { + using (LogContext.PushProperty("Archive", arc.FileName)) + Logger.Information("Processing archive"); + var arcDebugInfo = new + { + Name = arc.FileName, + NestedArchive = arc.IsInnerArchive, + Path = arc.IsInnerArchive ? arc.Path : arc?.FileInfo?.Path, + Extractor = arc.Extractor?.GetType().Name, + ParentArchiveNestedArchive = arc?.AssociatedArchive?.IsInnerArchive, + ParentArchiveName = arc?.AssociatedArchive?.FileName, + ParentArchivePath = arc?.AssociatedArchive?.IsInnerArchive ?? false ? arc?.AssociatedArchive?.Path : + arc?.AssociatedArchive?.FileInfo?.Path, + ParentExtractor = arc?.AssociatedArchive?.Extractor?.GetType().Name, + }; + Logger.Debug("Archive that is about to be processed: {@Arc}", arcDebugInfo); + if (CancellationToken.IsCancellationRequested) { HandleEarlyExit(); return; } + + State = ProcessorState.Starting; + try + { + FileSystem.CreateDirectoryInfo(TempLocation).Create(); + } + catch (Exception e) + { + EmitOnProcessError(new DPProcessorErrorArgs(e, "Unable to create temp directory.") { Continuable = true }); + } + + State = ProcessorState.Peeking; + if (ArchiveCancelled) { HandleEarlyExit(); return; } + if (arc.Extractor is null) + { + EmitOnProcessError(new DPProcessorErrorArgs(null, "Unable to process archive. Potentially not an archive or archive is corrupted.")); + HandleEarlyExit(); + continue; + } + arc.Extractor.CancellationToken = ArchiveCancellationToken; + if (!tryCatch(() => arc!.PeekContents(), "Failed to peek into archive")) continue; + + // Check if we have enough room. + if (!HandleOnDestinationNotEnoughSpace()) + { + HandleEarlyExit(); + continue; + } + + State = ProcessorState.PreparingExtraction; + HashSet filesToExtract = null!; + if (!tryCatch(prepareOperations, "Failed to prepare for extraction")) continue; + if (ArchiveCancelled) { HandleEarlyExit(); continue; } + if (!tryCatch(() => filesToExtract = DestinationDeterminer.DetermineDestinations(arc, settings), "Failed to determine destinations for files")) continue; + + State = ProcessorState.Extracting; + var extractSettings = new DPExtractSettings() + { + TempPath = CurrentProcessSettings.TempPath, + Archive = arc, + FilesToExtract = filesToExtract, + OverwriteFiles = CurrentProcessSettings.OverwriteFiles, + CancelToken = ArchiveCancellationToken, + }; + + if (ArchiveCancelled) { HandleEarlyExit(); continue; } + if (!tryCatch(() => report = arc.ExtractContents(extractSettings), "Failed to extract contents for archive")) continue; + + // DPCommon.WriteToLog("We are done"); + Logger.Information("Analyzing the archive - fetching tags"); + State = ProcessorState.Analyzing; + if (!tryCatch(() => arc.Type = arc.DetermineArchiveType(), "Failed to analyze archive")) continue; + if (!tryCatch(() => TagProvider.GetTags(arc, settings), "Failed to get tags for archive")) continue; + + foreach (var subarc in arc.Subarchives.Where(x => x.Extracted)) + { + archivesToProcess.Push(subarc); + } + + // Create record. + parentArchives.Push(new Tuple(arc, report)); // TODO: Use method to determine whether an archive was successfully processed. + } + catch (Exception ex) + { + handleError(ex, "An unexpected error occured while processing archive."); + EmitOnArchiveExit(false, report); + } + } + + PopParentArchive(parentArchives); + + } + + // TODO: RetryArchive() + public void ProcessArchive(string filePath, DPProcessSettings settings) + { + validateProcessSettings(ref settings); + CurrentProcessSettings = settings; + FileSystem.Scope = setupScope(settings); + // Create new archive. + var archiveFile = DPArchive.CreateNewParentArchive(FileSystem.CreateFileInfo(filePath)); + CurrentArchive = archiveFile; + + processArchiveInternal(archiveFile, settings); + Finished?.Invoke(); + State = ProcessorState.Idle; + } + + /// + /// For testing. + /// + /// + /// + internal void ProcessArchive(DPArchive arc, DPProcessSettings settings) + { + validateProcessSettings(ref settings); + CurrentProcessSettings = settings; + FileSystem.Scope = setupScope(settings); + CurrentArchive = arc; + processArchiveInternal(arc, settings); + Finished?.Invoke(); + State = ProcessorState.Idle; + } + + /// + /// Validates the object and throws an exception for any non-nullable properties that are null + /// and defaults any nullable, null properties to their default values. + /// + /// The settings to check and manipulate (will modify if nullable, null properties are detected)./> + private static void validateProcessSettings(ref DPProcessSettings settings) + { + ArgumentNullException.ThrowIfNull(settings.DestinationPath, nameof(settings.DestinationPath)); + ArgumentNullException.ThrowIfNull(settings.TempPath, nameof(settings.TempPath)); + ArgumentNullException.ThrowIfNull(settings.ForceFileToDest, nameof(settings.ForceFileToDest)); + settings.ContentRedirectFolders ??= new(DefaultRedirects, StringComparer.OrdinalIgnoreCase); + settings.ContentFolders ??= new(DefaultContentFolders, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Creates a scope based off of DPProcessSettings. + /// + /// + private static DPFileScopeSettings setupScope(DPProcessSettings settings) + { + var filesToAllow = new List(settings.ForceFileToDest.Values); + return new DPFileScopeSettings(filesToAllow, new[] { settings.DestinationPath, settings.TempPath }, false, false, true, false); + } + + /// + /// Checks whether the destination has enough space for the archive. + /// + /// True if the destination has enough space, otherwise false. + /// An exception caused by creating the object. + private bool DestinationHasEnoughSpace() => (ulong)FileSystem.CreateDriveInfo(CurrentProcessSettings.DestinationPath).AvailableFreeSpace > CurrentArchive.TrueArchiveSize; + + /// + /// Checks whether the temp path has enough space for the archive. + /// + /// True if temp has enough space, otherwise false. + /// An exception caused by creating the object. + private bool TempHasEnoughSpace() => (ulong)FileSystem.CreateDriveInfo(CurrentProcessSettings.TempPath).AvailableFreeSpace > CurrentArchive.TrueArchiveSize; + + + // TODO: Clear temp needs to remove as much space as possible. It will error when we have file handles. + private void ClearTemp() + { + Logger.Information("Clearing temp location at {TempLocation}", TempLocation); + var tmpScope = FileSystem.Scope; + FileSystem.Scope = new DPFileScopeSettings(Array.Empty(), new[] { TempLocation }, false, throwOnPathTransversal: true); + IDPDirectoryInfo info = FileSystem.CreateDirectoryInfo(TempLocation); + if (!TryHelper.Try(() => info.Delete(true), out Exception? ex)) + Logger.Error(ex, "Failed to clear temp location"); + else Logger.Information("Cleared temp location"); + FileSystem.Scope = tmpScope; + } + + private void prepareOperations() + { + Logger.Information("Preparing operations"); + while (!ArchiveCancelled && !TempHasEnoughSpace()) + { + ClearTemp(); + if (TempHasEnoughSpace()) break; + Logger.Warning("Temp location does not have enough space after clearing temp, requesting for an action"); + // Requires user help. + var args = new DPProcessorErrorArgs(null, "Temp location does not have enough space") { Continuable = true }; + EmitOnProcessError(args); + } + if (CurrentArchive.Extractor != null) CurrentArchive.Extractor.ExtractProgress += EmitOnExtractionProgress; + else Logger.Warning("Extractor is null, cannot report extraction progress"); + ReadMetaFiles(CurrentProcessSettings); + } + + private void HandleEarlyExit() + { + State = ProcessorState.Idle; + CurrentArchive.Extractor.ExtractProgress -= EmitOnExtractionProgress; + EmitOnArchiveExit(false, null); + } + + private bool HandleOnDestinationNotEnoughSpace() + { + if (DestinationHasEnoughSpace()) return true; + while (!ArchiveCancelled && !DestinationHasEnoughSpace()) + { + var args = new DPProcessorErrorArgs(null, "Destination does not have enough space.") { Continuable = true }; + EmitOnProcessError(args); + } + return !ArchiveCancelled || DestinationHasEnoughSpace(); + } + + /// + /// Executes an action and emits the error and handles the early exit procedure if an exception occurs. + /// + /// The action to execute. + /// The additional explanation of the error + /// Whether the try-catch caught an exception or not. + private bool tryCatch(Action action, string errorMessage) + { + if (!TryHelper.Try(action, out Exception? ex)) + { + handleError(ex, errorMessage); + return false; + } + return true; + } + + private void handleError(Exception ex, string errorMessage) + { + EmitOnProcessError(new DPProcessorErrorArgs(ex, errorMessage)); + HandleEarlyExit(); + return; + } + + /// + /// Reads the files listed in . + /// + private void ReadMetaFiles(DPProcessSettings settings) + { + // Extract the DAZ Files that have not been extracted. + var extractSettings = new DPExtractSettings(settings.TempPath, + CurrentArchive!.DSXFiles.Where((f) => f.FileInfo is null || !f.FileInfo.Exists), + true, CurrentArchive); + if (ArchiveCancelled) return; + CurrentArchive.ExtractContentsToTemp(extractSettings); + Stream? stream = null!; + foreach (DPDSXFile file in CurrentArchive!.DSXFiles) + { + if (ArchiveCancelled) return; + using (LogContext.PushProperty("File", file.Path)) + // If it did not extract correctly we don't have acces, just skip it. + if (file.FileInfo is null || !file.FileInfo.Exists) + { + Logger.Warning("FileInfo was null or returned does not exist, skipping file to read meta data", file.Path); + Logger.Debug("FileInfo is null: {0}, FileInfo exists: {1}", file.FileInfo is null, file?.FileInfo?.Exists); + continue; + } + try + { + if (!file.FileInfo!.TryAndFixOpenRead(out stream, out Exception? ex)) + { + EmitOnProcessError(new DPProcessorErrorArgs(ex, "Failed to open read stream for file for reading meta")); + continue; + } + if (stream is null) + { + EmitOnProcessError(new DPProcessorErrorArgs(null, "OpenRead returned successful but also returned null stream, skipping meta read")); + continue; + } + if (stream.ReadByte() == 0x1F && stream.ReadByte() == 0x8B) + { + // It is gzipped compressed. + stream.Seek(0, SeekOrigin.Begin); + using var gstream = new GZipStream(stream, CompressionMode.Decompress); + using var streamReader = new StreamReader(gstream, Encoding.UTF8, true); + file.CheckContents(streamReader); + } + else + { + // It is normal text. + stream.Seek(0, SeekOrigin.Begin); + using var streamReader = new StreamReader(stream, Encoding.UTF8, true); + file.CheckContents(streamReader); + } + } + catch (Exception ex) + { + EmitOnProcessError(new DPProcessorErrorArgs(ex, $"Unable to read contents of {file.Path}")); + } + finally + { + stream?.Dispose(); + } + } + } + + /// + /// Cancels the processing of the archive. + /// + public void CancelProcessing() + { + try + { + CancellationTokenSource.Cancel(); + ArchiveCancellationSource.Cancel(); + } + catch (Exception ex) + { + EmitOnProcessError(new DPProcessorErrorArgs(ex, "Failed to cancel processing")); + } + } + /// + /// Cancels only the current archive. + /// + public void CancelCurrentArchive() + { + try + { + ArchiveCancellationSource.Cancel(); + } catch (Exception ex) + { + EmitOnProcessError(new DPProcessorErrorArgs(ex, "Failed to cancel current archive")); + } + } + private void PopParentArchive(Stack> s) + { + if (s.TryPop(out var parentArc)) + { + var temp = CurrentArchive; + var report = parentArc.Item2; + CurrentArchive = parentArc.Item1; + try { EmitOnArchiveExit(report.SuccessPercentage >= 0.1f, report); } catch { } + CurrentArchive = temp; + } + } + + } +} diff --git a/src/DAZ_Installer.Core/DPProcessorErrorArgs.cs b/src/DAZ_Installer.Core/DPProcessorErrorArgs.cs new file mode 100644 index 0000000..8ab6093 --- /dev/null +++ b/src/DAZ_Installer.Core/DPProcessorErrorArgs.cs @@ -0,0 +1,37 @@ +namespace DAZ_Installer.Core +{ + /// + /// Represents error arguments for errors in DPProcessor outside out extraction errors. + /// + public class DPProcessorErrorArgs : EventArgs + { + /// + /// The exception thrown, if any. + /// + public Exception? Ex { get; init; } + /// + /// The processor that threw the error. + /// + public DPProcessor Processor { get; internal set; } = null!; + /// + /// Additional information for the error, if any. + /// + public string Explaination { get; internal set; } = string.Empty; + /// + /// Represents whether the operation can be continued or not. Default is false. + /// + public bool Continuable { get; internal set; } = false; + /// + /// + /// + /// The exception thrown by the error, if any. + /// The additional explaination for the error/situation. + /// The corresponding archive the + internal DPProcessorErrorArgs(Exception? ex = null, string? explaination = null) : base() + { + Ex = ex; + if (explaination != null) + Explaination = explaination; + } + } +} diff --git a/src/DAZ_Installer.Core/DPProcessorState.cs b/src/DAZ_Installer.Core/DPProcessorState.cs new file mode 100644 index 0000000..9370bd8 --- /dev/null +++ b/src/DAZ_Installer.Core/DPProcessorState.cs @@ -0,0 +1,30 @@ +namespace DAZ_Installer.Core +{ + public enum ProcessorState + { + /// + /// The processor is idle and not doing anything. + /// + Idle, + /// + /// The processor is starting up and preparing to process an archive (including nested). + /// + Starting, + /// + /// The processor is determining which files to extract and to where. + /// + PreparingExtraction, + /// + /// The processor has identified files to extract and is extracting them. + /// + Extracting, + /// + /// The processor is currently reading the files to extract. + /// + Peeking, + /// + /// The processor is analyzing the files, fetching tags, reading metadata, etc. + /// + Analyzing, + } +} diff --git a/src/DAZ_Installer.Core/DPProductInfo.cs b/src/DAZ_Installer.Core/DPProductInfo.cs new file mode 100644 index 0000000..dc5962a --- /dev/null +++ b/src/DAZ_Installer.Core/DPProductInfo.cs @@ -0,0 +1,44 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +namespace DAZ_Installer.Core +{ + public struct DPProductInfo + { + /// + /// The name of the product. + /// + public string ProductName = string.Empty; + /// + /// The tags auto-generated from the archive. + /// + public HashSet Tags = new(3); + /// + /// The authors identified from the archive. + /// + public HashSet Authors = new(1); + /// + /// The SKU of the product. + /// + public string SKU = string.Empty; + + /// + /// Creates a new instance of . + /// + public DPProductInfo() { } + /// + /// Creates a new instance of . + /// + /// The product name observed from the archive. + /// The authors identified from the archive. + /// The SKU of the product. + /// Tags generated based on parsed archive contents. + public DPProductInfo(string productName, HashSet? authors = null, string? sku = null, HashSet? tags = null) + { + if (tags != null) Tags = tags; + if (authors != null) Authors = authors; + if (sku != null) SKU = sku; + ProductName = productName; + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.Core/DPTagProvider.cs b/src/DAZ_Installer.Core/DPTagProvider.cs new file mode 100644 index 0000000..96eff9c --- /dev/null +++ b/src/DAZ_Installer.Core/DPTagProvider.cs @@ -0,0 +1,173 @@ +using DAZ_Installer.IO; +using System.IO.Compression; +using DAZ_Installer.Core.Extraction; +using System.Text; +using Serilog.Context; + +namespace DAZ_Installer.Core +{ + /// + /// Provides tags for s and updates them in the archive. + /// + public class DPTagProvider : AbstractTagProvider + { + /// + public override HashSet GetTags(DPArchive arc, DPProcessSettings settings) + { + // First is always author. + // Next is folder names. + + // Read DAZ files to see if we can get some juicy information such as + // the author, content type, website, email, etc. + ReadContentFiles(arc, settings); + // Read the meta files to get the product name(s) and other information. + ReadMetaFiles(arc, settings); + var tagsSet = new HashSet(StringComparer.OrdinalIgnoreCase); + tagsSet.EnsureCapacity(arc.GetEstimateTagCount() + 5 + + (arc.Folders.Count * 2) + ((arc.Contents.Count - arc.Subarchives.Count) * 2)); + foreach (DPDazFile file in arc.DazFiles) + { + DPContentInfo contentInfo = file.ContentInfo; + if (contentInfo.Website.Length != 0) tagsSet.Add(contentInfo.Website); + if (contentInfo.Email.Length != 0) tagsSet.Add(contentInfo.Email); + if (contentInfo.ContentType != ContentType.Unknown) tagsSet.Add(contentInfo.ContentType.ToString()); + tagsSet.UnionWith(contentInfo.Authors); // <-- I think this can be omitted since arc.ProductInfo includes + // the authors already. + } + foreach (DPFile content in arc.Contents.Values) + { + if (content is DPArchive) continue; + tagsSet.UnionWith(Path.GetFileNameWithoutExtension(content.FileName).Split(' ')); + } + foreach (KeyValuePair folder in arc.Folders) + { + tagsSet.UnionWith(PathHelper.GetFileName(folder.Key).Split(' ')); + } + tagsSet.UnionWith(arc.ProductInfo.Authors); + tagsSet.UnionWith(DPArchive.RegexSplitName(Path.GetFileNameWithoutExtension(arc.FileName))); + if (arc.ProductInfo.SKU.Length != 0) tagsSet.Add(arc.ProductInfo.SKU); + if (!string.IsNullOrEmpty(arc.ProductName)) tagsSet.Add(arc.ProductName); + return arc.ProductInfo.Tags = tagsSet; + } + + /// + /// Reads files that have the extension .dsf and .duf after it has been extracted. + /// + private void ReadContentFiles(DPArchive arc, DPProcessSettings settings) + { + // Extract the DAZ Files that have not been extracted. + var extractSettings = new DPExtractSettings(settings.TempPath, + arc!.DazFiles.Where(f => f.FileInfo is null || !f.FileInfo.Exists), + true, arc); + if (extractSettings.FilesToExtract.Count > 0) arc.ExtractContentsToTemp(extractSettings); + Stream? stream = null; + // Read the contents of the files. + foreach (DPDazFile file in arc!.DazFiles.Where(f => f.FileInfo?.Exists ?? false)) + { + using (LogContext.PushProperty("File", file.Path)) + // If it did not extract correctly we don't have acces, just skip it. + if (file.FileInfo is null || !file.FileInfo.Exists) + { + Logger.Error("File does not exist on disk (or does not have access to it)."); + continue; + } + try + { + if (!file.FileInfo!.TryAndFixOpenRead(out stream, out Exception? ex)) + { + Logger.Error(ex, $"Failed to open read stream for file: {file.Path}"); + continue; + } + if (stream is null) + { + Logger.Error($"OpenRead returned successful but also returned null stream, skipping {file.Path}"); + continue; + } + if (stream.ReadByte() == 0x1F && stream.ReadByte() == 0x8B) + { + // It is gzipped compressed. + stream.Seek(0, SeekOrigin.Begin); + using var gstream = new GZipStream(stream, CompressionMode.Decompress); + using var streamReader = new StreamReader(gstream, Encoding.UTF8, true); + file.ReadContents(streamReader); + } + else + { + // It is normal text. + stream.Seek(0, SeekOrigin.Begin); + using var streamReader = new StreamReader(stream, Encoding.UTF8, true); + file.ReadContents(streamReader); + } + } + catch (Exception ex) + { + Logger.Error(ex, $"Unable to read contents of {file}"); + } + finally + { + stream?.Dispose(); + } + } + } + + /// + /// Reads the files listed in . + /// + private void ReadMetaFiles(DPArchive arc, DPProcessSettings settings) + { + // Extract the DAZ Files that have not been extracted. + var extractSettings = new DPExtractSettings(settings.TempPath, + arc!.DSXFiles.Where((f) => f.FileInfo is null || !f.FileInfo.Exists), + true, arc); + arc.ExtractContentsToTemp(extractSettings); + Stream? stream = null!; + foreach (DPDSXFile file in arc!.DSXFiles) + { + using (LogContext.PushProperty("File", file.Path)) + // If it did not extract correctly we don't have acces, just skip it. + if (file.FileInfo is null || !file.FileInfo.Exists) + { + Logger.Warning("FileInfo was null or returned does not exist, skipping file to read meta data", file.Path); + Logger.Debug("FileInfo is null: {0}, FileInfo exists: {1}", file.FileInfo is null, file?.FileInfo?.Exists); + continue; + } + try + { + if (!file.FileInfo!.TryAndFixOpenRead(out stream, out Exception? ex)) + { + Logger.Error(ex, $"Failed to open read stream for file for reading meta: {file.Path}"); + continue; + } + if (stream is null) + { + Logger.Error($"OpenRead returned successful but also returned null stream, skipping {file.Path}"); + continue; + } + if (stream.ReadByte() == 0x1F && stream.ReadByte() == 0x8B) + { + // It is gzipped compressed. + stream.Seek(0, SeekOrigin.Begin); + using var gstream = new GZipStream(stream, CompressionMode.Decompress); + using var streamReader = new StreamReader(gstream, Encoding.UTF8, true); + file.CheckContents(streamReader); + } + else + { + // It is normal text. + stream.Seek(0, SeekOrigin.Begin); + using var streamReader = new StreamReader(stream, Encoding.UTF8, true); + file.CheckContents(streamReader); + } + } + catch (Exception ex) + { + Logger.Error(ex, $"Unable to read contents of {file.Path}"); + } + finally + { + stream?.Dispose(); + } + } + } + } +} diff --git a/src/DAZ_Installer.Core/DPTaskManager.cs b/src/DAZ_Installer.Core/DPTaskManager.cs new file mode 100644 index 0000000..fe0bcb3 --- /dev/null +++ b/src/DAZ_Installer.Core/DPTaskManager.cs @@ -0,0 +1,320 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +namespace DAZ_Installer.Core +{ + // Notes about Tasks, TaskFactory: + // (1) Token still works even after being disposed. + // (2) Source still semi-works after dispose call. Token is "disposed". + // (3) Tasks will continue to run unless you explicitly use the token and current task scheduler. + public struct DPTaskManager + { + public delegate void QueueAction(CancellationToken token); + public delegate void QueueAction(T arg1, CancellationToken token); + public delegate void QueueAction(T1 arg1, T2 arg2, CancellationToken token); + public delegate void QueueAction(T1 arg1, T2 arg2, T3 arg3, CancellationToken token); + public delegate void QueueAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, CancellationToken token); + public delegate void QueueAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, CancellationToken token); + + + + private CancellationTokenSource _source; + private TaskFactory _taskFactory; + private CancellationToken _token; + private volatile Task? lastTask; + private object lockObj = new(); + // (3) Tasks will continue with continueWith() chain unless this is passed in. + private const TaskContinuationOptions _continuationOptions = TaskContinuationOptions.NotOnCanceled; + + // According to documentation, Scheduler may (and usually is) null, if null use Current property. + // Check each time by using property instead of raw field. + private TaskScheduler _scheduler => _taskFactory.Scheduler ?? TaskScheduler.Current; + + public DPTaskManager() + { + _source = new CancellationTokenSource(); + _token = _source.Token; + _taskFactory = new TaskFactory(_token); + lastTask = null; + } + + private void Reset() + { + lock (lockObj) + { + _source.Dispose(); + _source = new CancellationTokenSource(); + _token = _source.Token; + _taskFactory = new TaskFactory(_token); + lastTask = null; + } + } + + public void Stop() + { + lock (lockObj) + { + _source.Cancel(); + Reset(); + } + } + + public void StopAndWait() + { + lock (lockObj) + { + _source.Cancel(); + lastTask?.Wait(); + Reset(); + } + } + #region Queue methods + + public Task AddToQueue(Action action) + { + CancellationToken t = _token; + Task task; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(action, _token); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => action(), t, _continuationOptions, _scheduler); + } + } + return task; + } + public Task AddToQueue(QueueAction action) + { + CancellationToken t = _token; + Task task; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => action(t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => action(t), t, _continuationOptions, _scheduler); + } + } + return task; + } + + public Task AddToQueue(QueueAction action, T arg) + { + CancellationToken t = _token; + Task task; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => action(arg, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => action(arg, t), t, _continuationOptions, _scheduler); + } + } + return task; + } + + + public Task AddToQueue(QueueAction action, T1 arg1, T2 arg2) + { + CancellationToken t = _token; + Task task; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => action(arg1, arg2, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => action(arg1, arg2, t), t, _continuationOptions, _scheduler); + } + } + return task; + } + + public Task AddToQueue(QueueAction action, T1 arg1, T2 arg2, T3 arg3) + { + CancellationToken t = _token; + Task task = lastTask; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => action(arg1, arg2, arg3, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => action(arg1, arg2, arg3, t), + t, _continuationOptions, _scheduler); + } + } + return task; + } + + public Task AddToQueue(QueueAction action, T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + CancellationToken t = _token; + Task task = lastTask; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => action(arg1, arg2, arg3, arg4, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => action(arg1, arg2, arg3, arg4, t), + t, _continuationOptions, _scheduler); + } + } + return task; + } + + public Task AddToQueue(QueueAction action, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) + { + CancellationToken t = _token; + Task task = lastTask; + + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => action(arg1, arg2, arg3, arg4, arg5, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => action(arg1, arg2, arg3, arg4, arg5, t), + t, _continuationOptions, _scheduler); + } + } + return task; + } + + public Task AddToQueue(Func func) + { + CancellationToken t = _token; + Task task = lastTask; + + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => func(t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => func(t), + t, _continuationOptions, _scheduler); + } + } + return task; + } + public Task AddToQueue(Func func, T1 arg1) + { + CancellationToken t = _token; + Task task = lastTask; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => func(arg1, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => func(arg1, t), + t, _continuationOptions, _scheduler); + } + } + return task; + } + public Task AddToQueue(Func func, T1 arg1, T2 arg2) + { + CancellationToken t = _token; + Task task = lastTask; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => func(arg1, arg2, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => func(arg1, arg2, t), + t, _continuationOptions, _scheduler); + } + } + return task; + } + public Task AddToQueue(Func func, + T1 arg1, T2 arg2, T3 arg3) + { + CancellationToken t = _token; + Task task = lastTask; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => func(arg1, arg2, arg3, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => func(arg1, arg2, arg3, t), + t, _continuationOptions, _scheduler); + } + } + return task; + } + public Task AddToQueue(Func func, + T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + CancellationToken t = _token; + Task task = lastTask; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => func(arg1, arg2, arg3, arg4, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => func(arg1, arg2, arg3, arg4, t), + t, _continuationOptions, _scheduler); + } + } + return task; + } + public Task AddToQueue(Func func, + T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) + { + CancellationToken t = _token; + Task task = lastTask; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => func(arg1, arg2, arg3, arg4, arg5, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => func(arg1, arg2, arg3, arg4, arg5, t), + t, _continuationOptions, _scheduler); + } + } + return task; + } + #endregion + + + + } +} diff --git a/src/External/7za.exe b/src/DAZ_Installer.Core/External/7za.exe similarity index 100% rename from src/External/7za.exe rename to src/DAZ_Installer.Core/External/7za.exe diff --git a/src/External/RAR.cs b/src/DAZ_Installer.Core/External/RAR.cs similarity index 85% rename from src/External/RAR.cs rename to src/DAZ_Installer.Core/External/RAR.cs index df21cda..30221cf 100644 --- a/src/External/RAR.cs +++ b/src/DAZ_Installer.Core/External/RAR.cs @@ -1,13 +1,11 @@ -using System; -using System.IO; +using System.Collections; +using DAZ_Installer.Core.Extraction; using System.Runtime.InteropServices; -using System.Collections; -using DAZ_Installer.DP; namespace DAZ_Installer.External { // GC Collect - public class RAR : IDisposable + public class RAR : IRAR { #region Event Delegate Definitions @@ -15,27 +13,27 @@ public class RAR : IDisposable /// /// Represents the method that will handle data available events /// - public delegate void DataAvailableHandler(RAR sender, DataAvailableEventArgs e); + public delegate void DataAvailableHandler(IRAR sender, DataAvailableEventArgs e); /// /// Represents the method that will handle extraction progress events /// - public delegate void ExtractionProgressHandler(RAR sender, ExtractionProgressEventArgs e); + public delegate void ExtractionProgressHandler(IRAR sender, ExtractionProgressEventArgs e); /// /// Represents the method that will handle missing archive volume events /// - public delegate void MissingVolumeHandler(RAR sender, MissingVolumeEventArgs e); + public delegate void MissingVolumeHandler(IRAR sender, MissingVolumeEventArgs e); /// /// Represents the method that will handle new volume events /// - public delegate void NewVolumeHandler(RAR sender, NewVolumeEventArgs e); + public delegate void NewVolumeHandler(IRAR sender, NewVolumeEventArgs e); /// /// Represents the method that will handle new file notifications /// - public delegate void NewFileHandler(RAR sender, NewFileEventArgs e); + public delegate void NewFileHandler(IRAR sender, NewFileEventArgs e); /// /// Represents the method that will handle password required events /// - public delegate void PasswordRequiredHandler(RAR sender, PasswordRequiredEventArgs e); + public delegate void PasswordRequiredHandler(IRAR sender, PasswordRequiredEventArgs e); #endregion #region RAR DLL enumerations @@ -55,7 +53,7 @@ public enum OpenMode Extract = 1 } - internal enum RarError : uint + public enum RarError : uint { EndOfArchive = 10, InsufficientMemory = 11, @@ -70,25 +68,25 @@ internal enum RarError : uint BufferTooSmall = 20, UnknownError = 21, MissingPassword = 22, - InternalError = 23, + publicError = 23, BadPassword = 24 } - internal enum Operation : uint + public enum Operation : uint { Skip = 0, Test = 1, Extract = 2 } - internal enum VolumeMessage : uint + public enum VolumeMessage : uint { Ask = 0, Notify = 1 } [Flags] - internal enum ArchiveFlags : uint + public enum ArchiveFlags : uint { Volume = 0x1, // Volume attribute (archive volume) CommentPresent = 0x2, // Archive comment present @@ -101,7 +99,7 @@ internal enum ArchiveFlags : uint FirstVolume = 0x100 // 0x0100 - First volume (set only by RAR 3.0 and later) } - internal enum CallbackMessages : uint + public enum CallbackMessages : uint { VolumeChange = 0, ProcessData = 1, @@ -299,12 +297,12 @@ private static extern void RARSetPassword(IntPtr hArcData, #region Private fields private string archivePathName = string.Empty; - private IntPtr archiveHandle = new IntPtr(0); + private IntPtr archiveHandle = new(0); private bool retrieveComment = true; private string password = string.Empty; private string comment = string.Empty; private ArchiveFlags archiveFlags = 0; - private RARHeaderDataEx header = new RARHeaderDataEx(); + private RARHeaderDataEx header = new(); private string destinationPath = string.Empty; private RARFileInfo currentFile = null; private UNRARCallback callback = null; @@ -313,15 +311,9 @@ private static extern void RARSetPassword(IntPtr hArcData, #region Object lifetime procedures - public RAR() - { - callback = new UNRARCallback(RARCallback); - } + public RAR() => callback = new UNRARCallback(RARCallback); - public RAR(string archivePathName) : this() - { - this.archivePathName = archivePathName; - } + public RAR(string archivePathName) : this() => this.archivePathName = archivePathName; ~RAR() { @@ -350,51 +342,27 @@ public void Dispose() ///
public string ArchivePathName { - get - { - return archivePathName; - } - set - { - archivePathName = value; - } + get => archivePathName; + set => archivePathName = value; } /// /// Archive comment /// - public string Comment - { - get - { - return comment; - } - } + public string Comment => comment; /// /// Current file being processed /// - public RARFileInfo CurrentFile - { - get - { - return currentFile; - } - } + public RARFileInfo CurrentFile => currentFile; /// /// Default destination path for extraction /// public string DestinationPath { - get - { - return destinationPath; - } - set - { - destinationPath = value; - } + get => destinationPath; + set => destinationPath = value; } /// @@ -402,10 +370,7 @@ public string DestinationPath /// public string Password { - get - { - return password; - } + get => password; set { password = value; @@ -416,7 +381,7 @@ public string Password /// /// Archive data /// - public RAROpenArchiveDataEx arcData; + public RAROpenArchiveDataEx ArchiveData { get; private set; } #endregion @@ -432,7 +397,7 @@ public void Close() return; // Close archive - int result = RARCloseArchive(archiveHandle); + var result = RARCloseArchive(archiveHandle); // Check result if (result != 0) @@ -481,7 +446,7 @@ public void Open(string archivePathName, OpenMode openMode) // Prepare extended open archive struct ArchivePathName = archivePathName; - RAROpenArchiveDataEx openStruct = new RAROpenArchiveDataEx(); + var openStruct = new RAROpenArchiveDataEx(); openStruct.Initialize(); openStruct.ArcName = this.archivePathName + "\0"; openStruct.ArcNameW = this.archivePathName + "\0"; @@ -499,7 +464,7 @@ public void Open(string archivePathName, OpenMode openMode) // Open archive handle = RAROpenArchiveEx(ref openStruct); - arcData = openStruct; + ArchiveData = openStruct; // Check for success if (openStruct.OpenResult != 0) { @@ -554,7 +519,7 @@ public bool ReadHeader() // Read next entry currentFile = null; - int result = RARReadHeaderEx(archiveHandle, ref header); + var result = RARReadHeaderEx(archiveHandle, ref header); // Check for error or end of archive if ((RarError)result == RarError.EndOfArchive) @@ -601,7 +566,7 @@ public bool ReadHeader() } catch (Exception e) { - DPCommon.WriteToLog(e); + // DPCommon.WriteToLog(e); // Headers are encrypted. // Close archive and try again. return false; @@ -618,14 +583,14 @@ public bool ReadHeader() /// public string[] ListFiles() { - ArrayList fileNames = new ArrayList(); + var fileNames = new ArrayList(); while (ReadHeader()) { if (!currentFile.IsDirectory) fileNames.Add(currentFile.FileName); Skip(); } - string[] files = new string[fileNames.Count]; + var files = new string[fileNames.Count]; fileNames.CopyTo(files); return files; } @@ -636,7 +601,7 @@ public string[] ListFiles() /// public void Skip() { - int result = RARProcessFileW(archiveHandle, (int)Operation.Skip, string.Empty, string.Empty); + var result = RARProcessFileW(archiveHandle, (int)Operation.Skip, string.Empty, string.Empty); // Check result if (result != 0) @@ -651,7 +616,7 @@ public void Skip() /// public void Test() { - int result = RARProcessFileW(archiveHandle, (int)Operation.Test, string.Empty, string.Empty); + var result = RARProcessFileW(archiveHandle, (int)Operation.Test, string.Empty, string.Empty); // Check result if (result != 0) @@ -664,30 +629,21 @@ public void Test() /// Extracts the current file to the default destination path /// /// - public void Extract() - { - Extract(destinationPath, string.Empty); - } + public void Extract() => Extract(destinationPath, string.Empty); /// /// Extracts the current file to a specified destination path and filename /// /// Path and name of extracted file /// - public void Extract(string destinationName) - { - Extract(string.Empty, destinationName); - } + public void Extract(string destinationName) => Extract(string.Empty, destinationName); /// /// Extracts the current file to a specified directory without renaming file /// /// /// - public void ExtractToDirectory(string destinationPath) - { - Extract(destinationPath, string.Empty); - } + public void ExtractToDirectory(string destinationPath) => Extract(destinationPath, string.Empty); #endregion @@ -695,7 +651,7 @@ public void ExtractToDirectory(string destinationPath) private void Extract(string destinationPath, string destinationName) { - int result = RARProcessFileW(archiveHandle, (int)Operation.Extract, destinationPath, destinationName); + var result = RARProcessFileW(archiveHandle, (int)Operation.Extract, destinationPath, destinationName); // Check result if (result != 0) @@ -706,12 +662,12 @@ private void Extract(string destinationPath, string destinationName) private DateTime FromMSDOSTime(uint dosTime) { - int day = 0; - int month = 0; - int year = 0; - int second = 0; - int hour = 0; - int minute = 0; + var day = 0; + var month = 0; + var year = 0; + var second = 0; + var hour = 0; + var minute = 0; ushort hiWord; ushort loWord; hiWord = (ushort)((dosTime & 0xFFFF0000) >> 16); @@ -755,7 +711,7 @@ private void ProcessFileError(int result) case RarError.MissingPassword: case RarError.BadPassword: throw new IOException("Password was incorrect."); - case RarError.InternalError: + case RarError.publicError: throw new IOException("Reference error."); } @@ -763,14 +719,15 @@ private void ProcessFileError(int result) private int RARCallback(uint msg, int UserData, IntPtr p1, int p2) { - string volume = string.Empty; - string newVolume = string.Empty; - int result = -1; + var volume = string.Empty; + var newVolume = string.Empty; + var result = -1; switch ((CallbackMessages)msg) { + case CallbackMessages.VolumeChange: case CallbackMessages.VolumeChangeAgain: - volume = Marshal.PtrToStringUni(p1); // Volume it was expecting. + volume = (CallbackMessages) msg == CallbackMessages.VolumeChange ? Marshal.PtrToStringUni(p1) : Marshal.PtrToStringUni(p1); if ((VolumeMessage)p2 == VolumeMessage.Notify) result = OnNewVolume(volume); else if ((VolumeMessage)p2 == VolumeMessage.Ask) @@ -783,12 +740,13 @@ private int RARCallback(uint msg, int UserData, IntPtr p1, int p2) if (newVolume != volume) { // Encode to Unicode. - var lilEndian = System.Text.Encoding.Unicode; + System.Text.Encoding lilEndian = System.Text.Encoding.Unicode; var bytes = lilEndian.GetBytes(newVolume + "\0"); - for (int i = 0; i < bytes.Length; i++) + for (var i = 0; i < bytes.Length; i++) { Marshal.WriteByte(p1, i, bytes[i]); } + Marshal.WriteByte(p1, newVolume.Length, 0); } result = 1; } @@ -816,21 +774,21 @@ protected virtual void OnNewFile() { if (NewFile != null) { - NewFileEventArgs e = new NewFileEventArgs(currentFile); + var e = new NewFileEventArgs(currentFile); NewFile(this, e); } } protected virtual int OnPasswordRequired(IntPtr p1, int p2) { - int result = -1; + var result = -1; if (PasswordRequired != null) { - PasswordRequiredEventArgs e = new PasswordRequiredEventArgs(); + var e = new PasswordRequiredEventArgs(); PasswordRequired(this, e); if (e.ContinueOperation && e.Password.Length > 0) { - for (int i = 0; i < e.Password.Length && i < p2; i++) + for (var i = 0; i < e.Password.Length && i < p2; i++) Marshal.WriteByte(p1, i, (byte)e.Password[i]); Marshal.WriteByte(p1, e.Password.Length, 0); result = 1; @@ -844,17 +802,17 @@ protected virtual int OnPasswordRequired(IntPtr p1, int p2) } protected virtual int OnPasswordRequiredLilE(IntPtr p1, int p2) { - int result = -1; + var result = -1; if (PasswordRequired != null) { - PasswordRequiredEventArgs e = new PasswordRequiredEventArgs(); + var e = new PasswordRequiredEventArgs(); PasswordRequired(this, e); if (e.ContinueOperation && e.Password.Length > 0) { - var lilEndian = System.Text.Encoding.Unicode; + System.Text.Encoding lilEndian = System.Text.Encoding.Unicode; var bytes = lilEndian.GetBytes(e.Password + "\0"); - for (int i = 0; i < bytes.Length && i < p2; i++) Marshal.WriteByte(p1, i, bytes[i]); + for (var i = 0; i < bytes.Length && i < p2; i++) Marshal.WriteByte(p1, i, bytes[i]); //Marshal.WriteByte(p1, e.Password.Length, (byte)18); result = 1; } @@ -868,21 +826,21 @@ protected virtual int OnPasswordRequiredLilE(IntPtr p1, int p2) protected virtual int OnDataAvailable(IntPtr p1, int p2) { - int result = 1; + var result = 1; if (currentFile != null) currentFile.BytesExtracted += p2; if (DataAvailable != null) { - byte[] data = new byte[p2]; + var data = new byte[p2]; Marshal.Copy(p1, data, 0, p2); - DataAvailableEventArgs e = new DataAvailableEventArgs(data); + var e = new DataAvailableEventArgs(data); DataAvailable(this, e); if (!e.ContinueOperation) result = -1; } if (ExtractionProgress != null && currentFile != null) { - ExtractionProgressEventArgs e = new ExtractionProgressEventArgs(); + var e = new ExtractionProgressEventArgs(); e.FileName = currentFile.FileName; e.FileSize = currentFile.UnpackedSize; e.BytesExtracted = currentFile.BytesExtracted; @@ -896,10 +854,10 @@ protected virtual int OnDataAvailable(IntPtr p1, int p2) protected virtual int OnNewVolume(string volume) { - int result = 1; + var result = 1; if (NewVolume != null) { - NewVolumeEventArgs e = new NewVolumeEventArgs(volume); + var e = new NewVolumeEventArgs(volume); NewVolume(this, e); if (!e.ContinueOperation) result = -1; @@ -909,10 +867,10 @@ protected virtual int OnNewVolume(string volume) protected virtual string OnMissingVolume(string volume) { - string result = string.Empty; + var result = string.Empty; if (MissingVolume != null) { - MissingVolumeEventArgs e = new MissingVolumeEventArgs(volume); + var e = new MissingVolumeEventArgs(volume); MissingVolume(this, e); if (e.ContinueOperation) result = e.VolumeName; @@ -930,10 +888,7 @@ public class NewVolumeEventArgs public string VolumeName; public bool ContinueOperation = true; - public NewVolumeEventArgs(string volumeName) - { - VolumeName = volumeName; - } + public NewVolumeEventArgs(string volumeName) => VolumeName = volumeName; } public class MissingVolumeEventArgs @@ -941,10 +896,7 @@ public class MissingVolumeEventArgs public string VolumeName; public bool ContinueOperation = false; - public MissingVolumeEventArgs(string volumeName) - { - VolumeName = volumeName; - } + public MissingVolumeEventArgs(string volumeName) => VolumeName = volumeName; } public class DataAvailableEventArgs @@ -952,10 +904,7 @@ public class DataAvailableEventArgs public readonly byte[] Data; public bool ContinueOperation = true; - public DataAvailableEventArgs(byte[] data) - { - Data = data; - } + public DataAvailableEventArgs(byte[] data) => Data = data; } public class PasswordRequiredEventArgs @@ -967,10 +916,7 @@ public class PasswordRequiredEventArgs public class NewFileEventArgs { public RARFileInfo fileInfo; - public NewFileEventArgs(RARFileInfo fileInfo) - { - this.fileInfo = fileInfo; - } + public NewFileEventArgs(RARFileInfo fileInfo) => this.fileInfo = fileInfo; } public class ExtractionProgressEventArgs diff --git a/src/DAZ_Installer.Core/Extraction/DP7zExtractor.cs b/src/DAZ_Installer.Core/Extraction/DP7zExtractor.cs new file mode 100644 index 0000000..0b6fb92 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/DP7zExtractor.cs @@ -0,0 +1,454 @@ +using DAZ_Installer.IO; +using System.Diagnostics; +using Serilog; +using Serilog.Context; + + +namespace DAZ_Installer.Core.Extraction +{ + public class DP7zExtractor : DPAbstractExtractor + { + public override ILogger Logger { get; set; } = Log.Logger.ForContext(); + internal IProcessFactory Factory { get; init; } = new ProcessFactory(); + private struct Entity + { + public string Path; + public bool isDirectory; + + public bool IsEmpty => Path == null; + } + private bool _hasEncryptedFiles = false; + private bool _hasEncryptedHeader = false; + private volatile IProcess? _process = null; + private bool _processHasStarted = false; + private string _arcPassword = string.Empty; + + // Peek phase variables. + private bool _peekFinished = false; + + // Extract phase variables. + private bool _extractFinished = false; + private bool _extractEventEmitted = false; + + // Extract phase variables. + private bool _moveFinished = false; + private bool _movingEventEmitted = false; + + private bool _seekingFiles = false; + + private bool _stopListening = false; + + private Entity _lastEntity = new() { }; + private DPExtractionReport workingExtractionReport = null!; + private DPArchive workingArchive = null!; + + // Flag + private bool tempOnly = false; + private DPExtractSettings workingSettings => workingExtractionReport.Settings; + private string tempFolder => Path.Combine(workingSettings.TempPath, Path.GetFileNameWithoutExtension(workingArchive.FileName)); + + public DP7zExtractor() { } + /// + /// Constructor for testing + /// + internal DP7zExtractor(ILogger logger, IProcessFactory factory) => (Logger, Factory) = (logger, factory); + public override DPExtractionReport Extract(DPExtractSettings settings) + { + using var _ = LogContext.PushProperty("Archive", settings.Archive.FileName); + return extractInternal(settings, false); + } + + public override DPExtractionReport ExtractToTemp(DPExtractSettings settings) + { + using var _ = LogContext.PushProperty("Archive", settings.Archive.FileName); + return extractInternal(settings, true); + } + + public override void Peek(DPArchive archive) + { + using var _ = LogContext.PushProperty("Archive", archive.FileName); + Reset(); + Logger.Information("Preparing to peek"); + mode = Mode.Peek; + workingArchive = archive; + FileSystem = archive.FileSystem; + EmitOnPeeking(); + _process = Setup7ZProcess(); + if (StartProcess()) + { + var time = TimeSpan.FromSeconds(120); + if (!SpinWait.SpinUntil(() => _peekFinished || CancellationToken.IsCancellationRequested, time)) + handleError(archive, $"Peek timeout of {time.TotalSeconds} seconds exceeded.", null, null, null); + } + KillProcess(); + EmitOnPeekFinished(); + Logger.Information("Peek finished"); + } + + private bool StartProcess() + { + Logger.Information("Starting 7z process"); + try + { + _process!.Start(); + _processHasStarted = true; + _process.BeginOutputReadLine(); + _process.BeginErrorReadLine(); + _process.StandardInput.WriteLineAsync(_arcPassword); + } + catch (Exception ex) + { + handleError(workingArchive, "Failed to start 7z process", null, null, ex); + return false; + } + return true; + } + + /// + /// Attempts to kill the process, dispose of it, and set it to null. + /// + private void KillProcess() + { + if (_process is not null) Logger.Information("Killing 7z process"); + if (_process is not null && !_process.HasExited) + Logger.Warning("7z process is being killed while it is not exited"); + try + { + if (_process is null) return; + _process.Kill(true); + _process.Dispose(); + _process.OutputDataReceived -= Handle7zOutput; + _process.ErrorDataReceived -= Handle7zErrors; + } catch (Exception ex) + { + Logger.Error(ex, "Failed to kill 7z process"); + } + _process = null; + } + + /// + /// Creates a new 7z process object depending on the current mode. + /// If the current mode is Peek, then it will tell 7z to list contents. + /// Otherwise, it will tell 7z to extract contents. + /// + /// A 7z process. + private IProcess Setup7ZProcess() + { + var process = Factory.Create(); + process.StartInfo.FileName = "7za.exe"; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardInput = true; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.CreateNoWindow = true; + process.EnableRaisingEvents = true; + process.OutputDataReceived += Handle7zOutput; + process.ErrorDataReceived += Handle7zErrors; + + if (mode == Mode.Peek) + process.StartInfo.ArgumentList.Add("l"); + else + process.StartInfo.ArgumentList.Add("x"); + process.StartInfo.ArgumentList.Add("-aoa"); // Overwrite existing files w/o prompt. + process.StartInfo.ArgumentList.Add("-slt"); // Show technical information. + // process.StartInfo.ArgumentList.Add("-bb1"); // Show names of processed files in log. + + process.StartInfo.ArgumentList.Add(workingArchive.FileInfo!.Path); + return process; + } + + private void Handle7zErrors(string? data) + { + if (_stopListening) return; + Logger.Debug("7z error output: {output}", data ?? "null"); + if (data is null) _peekFinished = true; + ReadOnlySpan msg = data; + + if (msg.Contains("Can not open encrypted archive. Wrong password?")) + handleError(workingArchive, DPArchiveErrorArgs.EncryptedArchiveExplanation, null, null, null); + // DPCommon.WriteToLog($"Handle 7z errors called! Msg: {e.Data}"); + } + + /// + /// Handles the appropriate action when receiving data from StandardOutput. + /// + private void Handle7zOutput(string? data) + { + using var _ = LogContext.PushProperty("Archive", workingArchive.FileName); + + if (CancellationToken.IsCancellationRequested || _stopListening) return; + Logger.Debug("7z output: {output}", data ?? "null"); + + var isEncrypted = _hasEncryptedFiles || _hasEncryptedHeader; + if (data == null) + { + if (mode == Mode.Peek) _peekFinished = true; + else _extractFinished = true; + + // Finalize the last 7z content. + if (data is null && !_lastEntity.IsEmpty) + { + FinalizeEntity(); + _lastEntity = new Entity { }; + } + if (tempOnly) finalizeTempOnlyOperation(); + return; + } + if (data.Contains("Enter password (will not be echoed):")) + { + handleEncryptedArchive(true); + return; + } + ReadOnlySpan msg = data; + if (mode == Mode.Peek) + { + if (msg.StartsWith("----------")) _seekingFiles = true; + if (!_seekingFiles) return; + + if (msg.StartsWith("Path")) + { + if (!_lastEntity.IsEmpty) FinalizeEntity(); + _lastEntity = new Entity { Path = msg.Slice(7).ToString() }; + } + else if (msg.StartsWith("Size")) + { + if (ulong.TryParse(msg.Slice(7), out var size)) + workingArchive.TrueArchiveSize += size; + } + else if (msg.StartsWith("Attributes")) + { + ReadOnlySpan attributes = msg.Slice("Attributes = ".Length); + _lastEntity.isDirectory = attributes.Contains("D"); + } + else if (msg.StartsWith("Encrypted = +")) handleEncryptedArchive(); + else if (msg.Contains("Errors:")) + Logger.Warning("7z reported errors while peeking"); + } + else + { + // Only check if everything really did extract if + var ok = msg.StartsWith("Everything is Ok"); + var errors = msg.Contains("Errors"); + if (!ok && !errors) return; + if (errors) Logger.Warning("7z reported errors while extracting"); + _extractFinished = true; + if (!tempOnly) MoveFiles(); + } + } + + /// + /// Emits an error due to encrypted archive and stops processing outputs. + /// + /// Set whether should be set to true or not. + private void handleEncryptedArchive(bool encryptedHeader = false) + { + handleError(workingArchive, DPArchiveErrorArgs.EncryptedFilesExplanation, null, null, null); + _extractFinished = _peekFinished = _moveFinished = _stopListening = true; + if (encryptedHeader) _hasEncryptedHeader = true; + else _hasEncryptedFiles = true; + } + + /// + /// Relocates the files from the temporary directory to their final destination. It also updates their file info. + /// + private void MoveFiles() + { + // Let anyone know that we are beginning to move files. + Logger.Information("Preparing to move files to destination"); + EmitOnMoving(); + _movingEventEmitted = true; + try + { + var i = 0UL; + var count = workingSettings.FilesToExtract.Count; + // For 7z specifically, we need to verify that the files were actually extracted and update their file info at the same time. + UpdateFileInfos(); + foreach (DPFile file in workingSettings.FilesToExtract) + { + CancellationToken.ThrowIfCancellationRequested(); + EmitOnMoveProgress(workingArchive, new DPExtractProgressArgs((byte)((float)i / count), workingArchive, file)); + IDPFileInfo? fileInfo = file.FileInfo; + // If the file did not extract to temp or it no longer exists (or we don't have access permissions), then we can't move it so we skip it. + if (fileInfo is null || !fileInfo.Exists) + { + Logger.Warning("{file} skipped due to failed extraction or insufficient permissions", file.Path); + Logger.Debug("FileInfo is null: {0} | FileInfo.Exists: {1}", fileInfo is null, fileInfo?.Exists); + continue; + } + // Create the directories needed so moving the file can be successful. + var targetDir = FileSystem.CreateDirectoryInfo(Path.GetDirectoryName(file.TargetPath)); + if (!targetDir.Exists && !targetDir.TryCreate()) + { + handleError(workingArchive, $"Failed to create directory for {file.Path}", file, workingExtractionReport, null); + continue; + } + if (fileInfo.TryAndFixMoveTo(file.TargetPath, true, out var ex)) workingExtractionReport.ExtractedFiles.Add(file); + else handleError(workingArchive, "Failed to move file to target location", file, workingExtractionReport, ex); + } + } + catch (Exception ex) + { + handleError(workingArchive, "An unknown error occured while attempting to move files to target location", null, null, ex); + } finally + { + _moveFinished = true; + } + } + + private void finalizeTempOnlyOperation() + { + UpdateFileInfos(); + foreach (DPFile file in workingSettings.FilesToExtract) + { + if (file.FileInfo is not null) workingExtractionReport.ExtractedFiles.Add(file); + else workingExtractionReport.ErroredFiles.Add(file, "Failed to extract file to temp directory"); + } + } + + /// + /// Resets the state of the extractor. + /// + private void Reset() + { + _moveFinished = _extractFinished = _movingEventEmitted = _extractEventEmitted = + _peekFinished = _seekingFiles = _hasEncryptedFiles = _hasEncryptedHeader = + _stopListening = false; + _lastEntity = new Entity { }; + KillProcess(); + _processHasStarted = false; + workingExtractionReport = null!; + workingArchive = null!; + tempOnly = false; + } + + /// + /// Updates all of the file infos in the archive that did extract. This needs to be called after the extraction is finished. + /// Since 7z extracts ALL files in the directory, we need to update all of the file infos. For example, if a file that originally wasn't + /// supposed to be moved to the user's library (e.g. unusual content folder) and now they want to move it, we can simply move the file + /// from the disk instead of extracting the entire archive again. + /// + private void UpdateFileInfos() + { + try + { + CancellationToken.ThrowIfCancellationRequested(); + foreach (DPFile file in workingArchive.Contents.Values) + { + if (file.AssociatedArchive != workingArchive) + { + handleError(workingArchive, string.Format(DPArchiveErrorArgs.FileNotPartOfArchiveErrorFormat, file.Path), file, workingExtractionReport, null); + Log.Debug("File {0} Associated Archive: {1}", file.FileName, file.AssociatedArchive?.Path); + continue; + } + var fileInfo = FileSystem.CreateFileInfo(Path.Combine(tempFolder, file.Path)); + if (fileInfo.Exists) file.FileInfo = fileInfo; // Set the file info even if we did not extract it to it's final dest. + else handleError(workingArchive, $"{file.Path} did not extract successfully", file, workingExtractionReport, null); + } + } + catch (Exception ex) + { + handleError(workingArchive, "Failed to update file infos in working archive", null, null, ex); + } + } + + /// + /// FinalizeEntity indicates that the last entity is finished and can make a or a + /// It has to be done this way because 7z seperates the attributes within each line and the the callback is called + /// for each line passed in. + /// + private void FinalizeEntity() + { + if (_lastEntity.isDirectory) + // Setting DPFolder to null will automatically create parent folders if they don't exist or + // automatically add the folder to the parent folder if it does exist. + new DPFolder(_lastEntity.Path, workingArchive, null); + else + DPFile.CreateNewFile(_lastEntity.Path, workingArchive, null); + } + + private DPExtractionReport StartExtractionProcess(string tempFolder, bool tempOnly = false) + { + try + { + EmitOnExtracting(); + _process = Setup7ZProcess(); + _process.StartInfo.ArgumentList.Add("-o" + tempFolder); + if (CancellationToken.IsCancellationRequested) return workingExtractionReport; + if (!StartProcess()) return workingExtractionReport; + var time = TimeSpan.FromSeconds(120); + var extractSuccessful = false; + if (!(extractSuccessful = SpinWait.SpinUntil(() => _extractFinished || CancellationToken.IsCancellationRequested, time))) + handleError(workingArchive, $"Extraction timeout of {time.TotalSeconds} seconds exceeded.", null, null, null); + EmitOnExtractFinished(); + _extractEventEmitted = true; + Logger.Information("Extract finished"); + if (tempOnly || !extractSuccessful || CancellationToken.IsCancellationRequested) return workingExtractionReport; + if (!SpinWait.SpinUntil(() => _moveFinished || CancellationToken.IsCancellationRequested, time)) + handleError(workingArchive, $"Move timeout of {time.TotalSeconds} seconds exceeded.", null, null, null); + } + finally + { + KillProcess(); + if (!_extractEventEmitted) EmitOnExtractFinished(); + if (_movingEventEmitted) + { + EmitOnMoveFinished(); + Logger.Information("Move finished"); + } + } + + return workingExtractionReport; + } + + /// + /// Logs the error, emits the error event, and adds the file to the report if it is not null. + /// + private void handleError(DPArchive arc, string msg, DPFile? file, DPExtractionReport? report, Exception? ex) + { + using var _ = LogContext.PushProperty("Archive", arc.FileName); + Logger.Error(ex, msg); + EmitOnArchiveError(arc, new DPArchiveErrorArgs(arc, ex, msg)); + if (file is not null && report is not null) + report.ErroredFiles.Add(file, msg); + } + + + private DPExtractionReport extractInternal(DPExtractSettings settings, bool extractToTemp) + { + Reset(); + Logger.Information(extractToTemp ? "Preparing to extract to temp" : "Preparing to extract"); + Logger.Debug("Extract(settings) = \n{@settings}", settings); + FileSystem = settings.Archive.FileSystem; + DPArchive archive = workingArchive = settings.Archive; + tempOnly = extractToTemp; + CancellationToken = settings.CancelToken; + if (archive.Contents.Count == 0) + { + Logger.Information("Archive Contents length was 0, now peeking..."); + Peek(archive); + } + mode = Mode.Extract; + // TODO: Log warning if process was interrupted while extracting. + workingExtractionReport = new DPExtractionReport() + { + ExtractedFiles = new(settings.FilesToExtract.Count), + Settings = settings + }; + if (CancellationToken.IsCancellationRequested) return workingExtractionReport; + if (archive.FileInfo is null || !archive.FileInfo.Exists) + { + handleError(archive, DPArchiveErrorArgs.ArchiveDoesNotExistOrNoAccessExplanation, null, null, null); + return workingExtractionReport; + } + var tempDir = FileSystem.CreateDirectoryInfo(tempFolder); + if (!tempDir.Exists && !TryHelper.Try(() => tempDir.Create(), out var ex)) + { + handleError(archive, "Failed to create required temp directories for extraction operations", null, null, ex); + return workingExtractionReport; + } + StartExtractionProcess(tempFolder, extractToTemp); + return workingExtractionReport; + } + + } +} diff --git a/src/DAZ_Installer.Core/Extraction/DPAbstractExtractor.cs b/src/DAZ_Installer.Core/Extraction/DPAbstractExtractor.cs new file mode 100644 index 0000000..e53f8ae --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/DPAbstractExtractor.cs @@ -0,0 +1,198 @@ +using DAZ_Installer.IO; +using Serilog; + +namespace DAZ_Installer.Core.Extraction +{ + /// + /// An abstract class for all extractors. + /// + public abstract class DPAbstractExtractor + { + /// + /// The logger to use for this extractor, if any. + /// + public virtual ILogger Logger { get; set; } = Log.Logger.ForContext(); + /// + /// The context to use for moving and (potentially) extracting files. + /// WARNING: Context may not be used to the full extent. For example, 7z requires all files be extracted to a temp location first, then moved to the final destination. + /// However, moving files is guaranteed to use this context to it's full extent. WinZip and RAR files extract directly to the final destination, so the context is used to it's full extent. + /// + public AbstractFileSystem FileSystem { get; protected set; } = new DPFileSystem(); + /// + /// The cancellation token to use for the extraction. By default, it is . + /// + public CancellationToken CancellationToken { get; set; } = CancellationToken.None; + /// + /// The current mode of the archive file; describes whether the archive is peeking, extracting, or moving files. + /// + protected enum Mode + { + /// + /// The archive is discovering files and folders. + /// + Peek, + /// + /// The archive is extracting files to a location. + /// + Extract, + /// + /// The archive is moving files from (usually) temp to its final destination. + /// + Moving + } + protected Mode mode; + /// + /// An event that is fired when an error occurs during extraction. + /// + public event DPArchiveEventHandler? ArchiveErrored; + /// + /// An event that is fired when the progress of extraction changes. + /// + public event DPArchiveEventHandler? ExtractProgress; + /// + /// An event that is fired when the progress of moving the files (from temp to dest) changes. + /// + public event DPArchiveEventHandler? MoveProgress; + /// + /// An event that is fired when the extractor is beginning to extract files, for some extractors, it may + /// need to extract files to a temporary location first. Check the event to know + /// when the files are being moved to the final destination. + /// + public event Action? Extracting; + /// + /// An event that is fired when the extractor is beginning to peek files. + /// + public event Action? Peeking; + /// + /// An event that is fired when the extractor is moving files to the final destination. This event may not + /// be invoked for some extractors. This event only occurs when the extractor is moving files from a temporary + /// location. + /// + public event Action? Moving; + /// + /// An event that is fired when the extractor is finished peeking files. + /// This event is ALWAYS invoked if, and only if, the Peeking event was invoked prior. + /// + public event Action? PeekFinished; + /// + /// An event that is fired when the extractor is finished extracting files. + /// This event is ALWAYS invoked if, and only if, the Extracting event was invoked prior. + /// + public event Action? ExtractFinished; + /// + /// An event that is fired when the extractor is finished moving files. + /// This event is ALWAYS invoked if, and only if, the Moving event was invoked prior. + /// + public event Action? MoveFinished; + /// + /// Attempts to extract files specifed in .
+ /// If the archive has no children, then the file will be peeked first via .
+ /// After that, the files will be extracted and a report will be returned when finished. + ///
+ /// The settings to use for extraction. + /// An extraction report indicating what files successfully extracted, what errored, etc. + public abstract DPExtractionReport Extract(DPExtractSettings settings); + /// + /// An optimized version of that extracts files to the temporary location defined + /// by . It does NOT extract to .
+ /// If the archive has no children, then the file will be peeked first + /// via .
After that, the files will be extracted and a report will be returned when finished. + ///
+ /// Settings to use for extraction. + /// + public abstract DPExtractionReport ExtractToTemp(DPExtractSettings settings); + /// + /// Checks to see if the archive exists (and accessible) and if it is a valid archive. It will create a new instance + /// of for you,
peeks the archive through and attempts + /// to extract via .
+ /// After that, the files will be extracted and a report will be returned when finished.
+ /// ADDITIONAL NOTE: will be overwritten to the new archive that is created. + ///
+ /// The settings to use for extraction. + /// The archive that you wish to extract from. + /// An extraction report indicating what files successfully extracted, what errored, etc. + public DPExtractionReport Extract(DPExtractSettings settings, IDPFileInfo archive) + { + ValidateArchive(archive); + var arc = DPArchive.CreateNewParentArchive(archive); + settings.Archive = arc; + return Extract(settings); + } + /// + /// Seeks the archive for it's contents and creates file objects and folders as children of .
+ /// You can check the contents found through .
+ ///
+ /// The archive you wish to seek files for. + public abstract void Peek(DPArchive archive); + /// + /// Checks to see if archive exists (and accessible) and if it is a valid archive. It will create a new + /// instance of for you, peeks the archive through , + /// and returns the instance. + /// + /// The archive that you wish to peek. + /// An archive object that is ready for extraction. + public DPArchive Peek(IDPFileInfo archive) + { + ValidateArchive(archive); + var arc = DPArchive.CreateNewParentArchive(archive); + Peek(arc); + return arc; + } + /// + /// Validates the archive to make sure it exists and is a valid archive. + /// + /// The archive that you wish to validate. + /// The archive does not exist or application does not have access to it. + /// The file is not an 7z, rar, or winzip archive after checking its file signature. + protected virtual void ValidateArchive(IDPFileInfo archive) + { + if (!archive.Exists) throw new FileNotFoundException("The archive file does not exist.", archive.Path); + if (DPArchive.DetermineArchiveFormatPrecise(archive.OpenRead(), true) == ArchiveFormat.Unknown) + throw new ArgumentException("The archive file is not a valid archive.", archive.Path); + } + /// + /// Invoke the event. + /// + protected virtual void EmitOnExtracting() => Extracting?.Invoke(); + /// + /// Invoke the event. + /// + protected virtual void EmitOnPeeking() => Peeking?.Invoke(); + /// + /// Invoke the event. + /// + protected virtual void EmitOnMoving() => Moving?.Invoke(); + /// + /// Invoke the event. + /// + /// The archive whose progress has changed. + /// The extraction args. + protected virtual void EmitOnExtractionProgress(DPArchive arc, DPExtractProgressArgs args) => ExtractProgress?.Invoke(arc, args); + /// + /// Invoke the event. + /// + /// The archive whose progress has changed. + /// The extraction args. + protected virtual void EmitOnMoveProgress(DPArchive arc, DPExtractProgressArgs args) => MoveProgress?.Invoke(arc, args); + /// + /// Invoke the event. + /// + /// The archive whose progress has changed. + /// The error args. + protected virtual void EmitOnArchiveError(DPArchive arc, DPArchiveErrorArgs args) => ArchiveErrored?.Invoke(arc, args); + /// + /// Invoke the event. + /// + protected virtual void EmitOnPeekFinished() => PeekFinished?.Invoke(); + /// + /// Invoke the event. + /// + protected virtual void EmitOnExtractFinished() => ExtractFinished?.Invoke(); + /// + /// Invoke the event. + /// + protected virtual void EmitOnMoveFinished() => MoveFinished?.Invoke(); + + public DPAbstractExtractor() { } + } +} diff --git a/src/DAZ_Installer.Core/Extraction/DPExtractProgressArgs.cs b/src/DAZ_Installer.Core/Extraction/DPExtractProgressArgs.cs new file mode 100644 index 0000000..e49a0fa --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/DPExtractProgressArgs.cs @@ -0,0 +1,29 @@ +namespace DAZ_Installer.Core +{ + /// + /// Represents the extraction progress arguments. + /// + public class DPExtractProgressArgs : EventArgs + { + /// + /// Percentage of the total extraction progress. + /// + public readonly byte ExtractionPercentage = 0; + /// + /// The archive that is currently extracting files. + /// + public readonly DPArchive Archive; + /// + /// The file that is currently being extracted from archive. Sometimes this is null. This can occur + /// when the archive has just finished the extraction process. + /// + public readonly DPAbstractNode? File; + + internal DPExtractProgressArgs(byte percent, DPArchive archive, DPAbstractNode? file) : base() + { + ExtractionPercentage = percent; + Archive = archive; + File = file; + } + } +} diff --git a/src/DAZ_Installer.Core/Extraction/DPExtractSettings.cs b/src/DAZ_Installer.Core/Extraction/DPExtractSettings.cs new file mode 100644 index 0000000..559f989 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/DPExtractSettings.cs @@ -0,0 +1,44 @@ +namespace DAZ_Installer.Core.Extraction +{ + /// + /// The extract settings for the archive to use. + /// + [Serializable] + public struct DPExtractSettings + { + /// + /// The temporary path to use for operations. Null not allowed.
+ /// This is used if the archive needs to be extracted to a temporary location before being moved to the destination. + ///
+ public string TempPath = string.Empty; + /// + /// Determines whether the extractor should overwrite files if it already exists in the user library. + /// Temp files will always be overwritten. + /// + public bool OverwriteFiles = true; + /// + /// A collection of files to extract. Files in this collection MUST BE IN . + /// + /// The file from the archive to extract. + public HashSet FilesToExtract = new(0); + /// + /// An archive to extract from. This can be implicitly set by . + /// All files in must be in this archive. + /// Or in other words, the
of all + /// files in must be this archive. + ///
+ public DPArchive Archive = null!; + /// + /// The cancellation token to use for the extraction. This setting will update . + /// + public CancellationToken CancelToken = CancellationToken.None; + + public DPExtractSettings(string? temp, IEnumerable filesToExtract, bool overwriteFiles = true, DPArchive? archive = null) + { + TempPath = temp ?? string.Empty; + OverwriteFiles = overwriteFiles; + FilesToExtract = new HashSet(filesToExtract); + Archive = archive ?? filesToExtract.FirstOrDefault()?.AssociatedArchive ?? throw new ArgumentException("No archive provided and no files to extract provided."); + } + } +} diff --git a/src/DAZ_Installer.Core/Extraction/DPExtractionReport.cs b/src/DAZ_Installer.Core/Extraction/DPExtractionReport.cs new file mode 100644 index 0000000..4a428f2 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/DPExtractionReport.cs @@ -0,0 +1,28 @@ +namespace DAZ_Installer.Core.Extraction +{ + /// + /// A report that is generated after the extraction process, even if the process has been interrupted. + /// + public record DPExtractionReport + { + /// + /// Files that have been successfully extracted. + /// + public List ExtractedFiles = new(0); + /// + /// The settings used for the extraction. + /// + public DPExtractSettings Settings; + /// + /// Errors that occurred while attempting to extract files. This may be empty if an error occured internally + /// before the extractor could extract the files. + /// + /// The file that errored while attempting to extract it. + /// The error message. + public Dictionary ErroredFiles = new(0); + /// + /// The percentage of files that successfully extracted. + /// + public float SuccessPercentage => (float)ExtractedFiles.Count / (ExtractedFiles.Count + ErroredFiles.Count); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/DPRARExtractor.cs b/src/DAZ_Installer.Core/Extraction/DPRARExtractor.cs new file mode 100644 index 0000000..715fc00 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/DPRARExtractor.cs @@ -0,0 +1,318 @@ +using DAZ_Installer.External; +using DAZ_Installer.IO; +using Serilog; +using Serilog.Context; + +namespace DAZ_Installer.Core.Extraction +{ + public class DPRARExtractor : DPAbstractExtractor + { + public override ILogger Logger { get; set; } = Log.Logger.ForContext(); + internal IRARFactory Factory { get; init; } = new RARFactory(); + private Session? session = null; + private class Session + { + public DPExtractSettings settings; + public DPExtractionReport report = new(); + public bool tempOnly = false; + } + + public DPRARExtractor() { } + /// + /// Constructor used for testing + /// + /// The factory to use for creating handlers. + internal DPRARExtractor(ILogger logger, IRARFactory factory) => (Logger, Factory) = (logger, factory); + #region Event Methods + + private void HandleMissingVolume(IRAR sender, MissingVolumeEventArgs e) + { + var msg = $"{sender.CurrentFile.FileName} is missing volume : {e.VolumeName}."; + Logger.Warning(msg); + var args = new DPArchiveErrorArgs(session.settings.Archive, null, msg); + EmitOnArchiveError(session.settings.Archive, args); + } + + + public void HandleNewFile(IRAR sender, NewFileEventArgs e) + { + try + { + if (e.fileInfo.IsDirectory) + { + if (session.settings.Archive.FolderExists(e.fileInfo.FileName)) return; + var f = new DPFolder(e.fileInfo.FileName, session.settings.Archive, null); + Logger.Debug("Discovered new directory: {0}", f.FileName); + } + else + { + var f = DPFile.CreateNewFile(e.fileInfo.FileName, session.settings.Archive, null); + Logger.Debug("Discovered new file: {0}", f.FileName); + } + } catch (Exception ex) + { + Logger.Error(ex, "Unexpected error occurred while handling new file: {0}", e.fileInfo.FileName); + } + } + + + #endregion + #region Override Methods + public override DPExtractionReport ExtractToTemp(DPExtractSettings settings) + { + Logger.Information("Extracting to temp"); + session = new Session() { settings = settings, report = new DPExtractionReport(), tempOnly = true }; + try + { + return Extract(settings); + } catch { throw; } + finally { session = null; } + } + public override DPExtractionReport Extract(DPExtractSettings settings) + { + using var _ = LogContext.PushProperty("Archive", settings.Archive.FileName); + Logger.Information("Preparing to extract"); + mode = Mode.Extract; + EmitOnExtracting(); + FileSystem = settings.Archive.FileSystem; + DPArchive arc = settings.Archive; + session ??= new Session() { report = new DPExtractionReport(), settings = settings }; + CancellationToken = settings.CancelToken; + + var report = new DPExtractionReport() + { + Settings = settings, + ExtractedFiles = new(settings.FilesToExtract.Count), + }; + session.report = report; + try + { + CancellationToken.ThrowIfCancellationRequested(); + if (arc.Contents.Count == 0) Peek(arc); + + if (arc.FileInfo is null || !arc.FileInfo.Exists) + { + handleError(arc, DPArchiveErrorArgs.ArchiveDoesNotExistOrNoAccessExplanation, report, null, null); + Logger.Debug("FileInfo is null: {0} | FileInfo.Exists: {1}", arc.FileInfo is null, arc.FileInfo?.Exists); + return report; + } + + using (var RARHandler = Factory.Create(arc.FileInfo.Path)) + { + try + { + // TODO: Update destination path. + RARHandler.Open(RAR.OpenMode.Extract); + var flags = (RAR.ArchiveFlags)RARHandler.ArchiveData.Flags; + var isFirstVolume = flags.HasFlag(RAR.ArchiveFlags.FirstVolume); + var isVolume = flags.HasFlag(RAR.ArchiveFlags.Volume); + + if (isVolume && !isFirstVolume) + handleError(arc, "The archive is not the first volume out of a multi-volume archive. Only input the first volume", null, null, null); + for (var i = 0; i < settings.FilesToExtract.Count && RARHandler.ReadHeader(); i++) + { + CancellationToken.ThrowIfCancellationRequested(); + var arcHasFile = arc.Contents.TryGetValue(PathHelper.NormalizePath(RARHandler.CurrentFile.FileName), out var file); + if (!RARHandler.CurrentFile.IsDirectory && arcHasFile && file!.AssociatedArchive == arc && settings.FilesToExtract.Contains(file)) + { + if (!ExtractFile(RARHandler, settings, report)) + RARHandler.Skip(); + EmitOnExtractionProgress(settings.Archive, new DPExtractProgressArgs((byte)((float)i / settings.FilesToExtract.Count), arc, file)); + } + else + { + if (arcHasFile && file!.AssociatedArchive != arc) + handleError(arc, string.Format(DPArchiveErrorArgs.FileNotPartOfArchiveErrorFormat, file.Path), report, file, null); + RARHandler.Skip(); + i--; + } + + } + RARHandler.Close(); + } + catch (OperationCanceledException) { } + catch (Exception e) + { + handleError(arc, "An unexpected error occured while processing the archive", null, null, e); + } + } + } + catch (Exception ex) + { + handleError(arc, "An unexpected error occurred before attempting to process the archive.", null, null, ex); + } finally + { + EmitOnExtractFinished(); + } + + return report; + } + public override void Peek(DPArchive arc) + { + using var _ = LogContext.PushProperty("Archive", arc.FileName); + Logger.Information("Preparing to peek"); + mode = Mode.Peek; + FileSystem = arc.FileSystem; + EmitOnPeeking(); + try + { + CancellationToken.ThrowIfCancellationRequested(); + + if (arc.FileInfo is null || !arc.FileInfo.Exists) + { + EmitOnArchiveError(arc, new DPArchiveErrorArgs(arc, null, DPArchiveErrorArgs.ArchiveDoesNotExistOrNoAccessExplanation)); + EmitOnPeekFinished(); + return; + } + using var RARHandler = Factory.Create(arc.FileInfo.Path); + RARHandler.MissingVolume += HandleMissingVolume; + RARHandler.NewFile += HandleNewFile; + session = new Session() { report = new DPExtractionReport(), settings = new() { Archive = arc, } }; + + // TODO: Can we remove this? + RARHandler.DestinationPath = Path.Combine(Path.GetTempPath(), Path.GetFileNameWithoutExtension(arc.Path)); + // Create path and see if it exists. + var dir = FileSystem.CreateDirectoryInfo(RARHandler.DestinationPath); + if (!dir.PreviewCreate()) Logger.Warning("The current destination directory is not whitelisted and will not be created"); + dir.TryCreate(); + + RARHandler.Open(RAR.OpenMode.List); + var flags = (RAR.ArchiveFlags)RARHandler.ArchiveData.Flags; + var isFirstVolume = flags.HasFlag(RAR.ArchiveFlags.FirstVolume); + var isVolume = flags.HasFlag(RAR.ArchiveFlags.Volume); + + if (isVolume && !isFirstVolume) + { + handleError(arc, "The archive is not the first volume out of a multi-volume archive. Only input the first volume.", null, null, null); + return; + } + + while (RARHandler.ReadHeader()) + { + CancellationToken.ThrowIfCancellationRequested(); + if (RARHandler.CurrentFile.IsDirectory) continue; + TestFile(RARHandler, arc); + } + RARHandler.Close(); + } + catch (OperationCanceledException) { } + catch (Exception e) + { + handleError(arc, "An unexpected error occured while processing the archive.", null, null, e); + } finally + { + EmitOnPeekFinished(); + } + } + #endregion + private bool ExtractFile(IRAR handler, DPExtractSettings settings, DPExtractionReport report) + { + var fileName = handler.CurrentFile.FileName; + DPArchive arc = settings.Archive; + + // Means that archive was modified while we were extracting. + if (!arc.Contents.TryGetValue(PathHelper.NormalizePath(fileName), out var file)) + { + handleError(arc, DPArchiveErrorArgs.FileNotPartOfArchiveErrorFormat, report, file, null); + return false; + } + IDPFileInfo? fileInfo = null!; + EXTRACT: + try + { + var targetPath = session!.tempOnly ? Path.Combine(settings.TempPath, + Path.GetFileNameWithoutExtension(arc.FileName), + fileName) : file.TargetPath; + handler.DestinationPath = Path.GetDirectoryName(targetPath)!; + // Create folders for the destination path if needed. + var dir = FileSystem.CreateDirectoryInfo(handler.DestinationPath); + if (!dir.Exists && !dir.TryCreate()) + { + handleError(arc, DPArchiveErrorArgs.UnauthorizedAccessExplanation, report, file, null); + return false; + } + + fileInfo ??= FileSystem.CreateFileInfo(targetPath); + if (!FileSystem.Scope.IsFilePathWhitelisted(targetPath)) + throw new OutOfScopeException(targetPath); + handler.Extract(targetPath); + + // Only update if we didn't error. + file.FileInfo = fileInfo; + report.ExtractedFiles.Add(file); + } + catch (IOException e) + { + var msg = string.Empty; + if (e.Message == "File CRC Error" || e.Message == "File could not be opened.") + { + var flags = (RAR.ArchiveFlags)handler.ArchiveData.Flags; + var isVolume = flags.HasFlag(RAR.ArchiveFlags.Volume); + var continuesNext = handler.CurrentFile.ContinuedOnNext; + var isEncrypted = handler.CurrentFile.encrypted; + msg = (!isVolume || !continuesNext) && !isEncrypted ? "Archive could be corrupt (or simply not an RAR file)." : string.Empty; + handleError(arc, msg, report, file, e); + } + // Check to see if we are attempting to overwrite a file that we don't have access to (ex: hidden/read-only/anti-virus/user no access). + else if (e.Message == "File write error." || e.Message == "File read error." || e.Message == "File could not be opened.") + { + fileInfo ??= FileSystem.CreateFileInfo(file.TargetPath); + if (fileInfo.TryAndFixOpenRead(out var stream, out Exception? ex)) + { + stream?.Dispose(); + goto EXTRACT; + } + else + { + msg = fileInfo.Exists ? "File could not be extracted and cannot overwrite existing file on disk due to file permissions." : + DPArchiveErrorArgs.UnauthorizedAccessAfterExplanation; + handleError(arc, msg, report, file, new AggregateException(e, ex)); + } + } + return false; + } catch (OutOfScopeException e) + { + handleError(arc, $"Extractor attempted to extract file to a destination that was not specified: {file.TargetPath}", report, file, e); + return false; + } catch (Exception e) + { + handleError(arc, "An unexpected error occured while processing the archive.", report, file, e); + return false; + } + return true; + } + + private bool TestFile(IRAR handler, DPArchive arc) + { + try + { + // I'm not sure if UnpackedSize returns negative if the file is partial. + arc.TrueArchiveSize += (ulong)Math.Max(0, handler.CurrentFile.UnpackedSize); + handler.Test(); + } + catch (IOException e) + { + if (e.Message == "File CRC Error" || e.Message == "File could not be opened.") + { + var flags = (RAR.ArchiveFlags)handler.ArchiveData.Flags; + var isVolume = flags.HasFlag(RAR.ArchiveFlags.Volume); + var continuesNext = handler.CurrentFile.ContinuedOnNext; + var isEncrypted = handler.CurrentFile.encrypted; + var msg = (!isVolume || !continuesNext) && !isEncrypted ? "Archive could be corrupt (or simply not an RAR file)." : string.Empty; + var associatedDPFile = arc.Contents.TryGetValue(PathHelper.NormalizePath(handler.CurrentFile.FileName), out var file) ? file : null; + handleError(arc, msg, null, associatedDPFile, e); + return false; + } + } + return true; + } + private void handleError(DPArchive arc, string msg, DPExtractionReport? report, DPFile? file, Exception? e) + { + using var _ = LogContext.PushProperty("Archive", arc.FileName); + Logger.Error(e, msg); + EmitOnArchiveError(arc, new DPArchiveErrorArgs(arc, e, msg)); + if (file is not null && report is not null) + report.ErroredFiles.Add(file, msg); + } + } +} diff --git a/src/DAZ_Installer.Core/Extraction/DPZipExtractor.cs b/src/DAZ_Installer.Core/Extraction/DPZipExtractor.cs new file mode 100644 index 0000000..40a7531 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/DPZipExtractor.cs @@ -0,0 +1,226 @@ + +using DAZ_Installer.IO; +using Serilog; +using Serilog.Context; + +namespace DAZ_Installer.Core.Extraction +{ + /// + /// An extractor for zip archives. + /// + public class DPZipExtractor : DPAbstractExtractor + { + public override ILogger Logger { get; set; } = Log.Logger.ForContext(); + internal virtual IZipArchiveFactory Factory { get; init; } = new ZipArchiveWrapperFactory(); + private bool tempOnly = false; + + public DPZipExtractor() { } + /// + /// Constructor used for testing + /// + /// The logger to use, if any. + /// The factory to use if any. + internal DPZipExtractor(ILogger logger, IZipArchiveFactory factory) => (Logger, Factory) = (logger, factory); + + public override DPExtractionReport Extract(DPExtractSettings settings) + { + using var _ = LogContext.PushProperty("Archive", settings.Archive.FileName); + Logger.Information("Preparing to extract"); + Logger.Debug("Extract(settings) = \n{@Settings}", settings); + + // Set up the extraction report to return in case of any issues. + var e = new DPExtractionReport() + { + ExtractedFiles = new(settings.FilesToExtract?.Count ?? 0), + Settings = settings + }; + try + { + // Reset any variables if needed. + FileSystem = settings.Archive.FileSystem; + // Peek into the archive if needed. + DPArchive arc = settings.Archive; + if (arc.Contents.Count == 0) Peek(arc); + + var max = settings.FilesToExtract.Count; + + if (CancellationToken.IsCancellationRequested) return e; + + // Check if the archive is on disk or we have access to it. + if (arc.FileInfo is null || !arc.FileInfo.Exists) + { + EmitOnArchiveError(arc, new DPArchiveErrorArgs(arc, null, DPArchiveErrorArgs.ArchiveDoesNotExistOrNoAccessExplanation)); + return e; + } + // Let listeners know that we are beginning to extract. + EmitOnExtracting(); + try + { + // Create the zip archive. + using var zipArc = Factory.Create(arc.FileInfo.OpenRead()); + + // Loop through all the files to extract and attempt to extract them. + var i = 0; + foreach (DPFile file in settings.FilesToExtract) + { + CancellationToken.ThrowIfCancellationRequested(); + // Check if the file is part of this archive, if not, emit an error and continue. + if (file.AssociatedArchive != settings.Archive) + { + HandleError(arc, file, e, null, string.Format(DPArchiveErrorArgs.FileNotPartOfArchiveErrorFormat, file.Path)); + Log.Debug("File {0} Associated Archive: {1}", file.FileName, file.AssociatedArchive?.Path); + continue; + } + // Extract the file. + ExtractFile(zipArc.GetEntry(file.Path), file, settings, e); + HandleProgressionZIP(file, ++i, max); + } + } + catch (Exception ex) + { + HandleError(arc, null, e, ex, "An unknown error occured while attempting to extract the archive"); + } + } catch (Exception ex) + { + Logger.Error(ex, "An unknown error occured while attempting to extract the archive"); + } finally + { + EmitOnExtractFinished(); + Logger.Information("Finished extracting"); + } + return e; + } + + public override DPExtractionReport ExtractToTemp(DPExtractSettings settings) + { + tempOnly = true; + try + { + return Extract(settings); + } catch + { + throw; + } finally + { + tempOnly = false; + } + } + + public override void Peek(DPArchive arc) + { + using var _ = LogContext.PushProperty("Archive", arc.FileName); + Logger.Information("Preparing to peek"); + // Emit that we are peeking. + EmitOnPeeking(); + // Reset any variables if needed. + arc.TrueArchiveSize = 0; + FileSystem = arc.FileSystem; + + try + { + if (CancellationToken.IsCancellationRequested) return; + + if (arc.FileInfo is null || !arc.FileInfo.Exists) + { + HandleError(arc, null, null, null, DPArchiveErrorArgs.ArchiveDoesNotExistOrNoAccessExplanation); + Logger.Debug("FileInfo is null: {0} | FileInfo.Exists: {1}", arc.FileInfo is null, arc.FileInfo?.Exists); + return; + } + + using var zipArc = Factory.Create(arc.FileInfo.OpenRead()); + foreach (var entry in zipArc.Entries) + { + CancellationToken.ThrowIfCancellationRequested(); + if (string.IsNullOrEmpty(entry.Name) && !arc.FolderExists(entry.FullName)) + // Set folder to null to let it automatically generate subfolders. + new DPFolder(entry.FullName, arc, null); + else DPFile.CreateNewFile(entry.FullName, arc, null); + arc.TrueArchiveSize += (ulong)Math.Max(0, entry.Length); + } + } catch (Exception ex) + { + HandleError(arc, null, null, ex, "An unknown error occured while attempting to peek the archive"); + } finally + { + EmitOnPeekFinished(); + Logger.Information("Finished peeking"); + } + } + + private void ExtractFile(IZipArchiveEntry? entry, DPFile file, DPExtractSettings settings, DPExtractionReport report) + { + var tryAgain = false; + DPArchive arc = settings.Archive; + if (entry is null) + { + HandleError(arc, file, report, null, string.Format(DPArchiveErrorArgs.FileNotPartOfArchiveErrorFormat, file.FileName)); + return; + } + + var expectedPath = tempOnly ? Path.Combine(settings.TempPath, Path.GetFileNameWithoutExtension(arc.Path), entry.Name) : file.TargetPath; + if (string.IsNullOrWhiteSpace(expectedPath)) + { + HandleError(arc, file, report, null, "Cannot perform extraction on empty target path"); + return; + } + IDPFileInfo? fileInfo = null!; + EXTRACT: + try + { + var dirInfo = FileSystem.CreateDirectoryInfo(Path.GetDirectoryName(expectedPath)!); + if (!dirInfo.Exists && !dirInfo.TryCreate()) Logger.Warning("Failed to create directory for {0}", dirInfo.Path); + // Extract the file and create the file info if it successfully extracted. + fileInfo ??= FileSystem.CreateFileInfo(expectedPath); + if (!FileSystem.Scope.IsFilePathWhitelisted(expectedPath)) + { + HandleError(arc, file, report, null, $"{expectedPath} is not whitelisted."); + return; + } + entry.ExtractToFile(expectedPath, settings.OverwriteFiles); + file.FileInfo = fileInfo; + report.ExtractedFiles.Add(file); + } + catch (IOException e) + { + if (e.Message.StartsWith("The file ") && e.Message.EndsWith("already exists")) + HandleError(arc, file, report, e, "Attempted to overwrite a file that exists but user chose not to overwrite files"); + else HandleError(arc, file, report, e, "Unknown error occurred while extracting file"); + } + // Note: System.UnauthorizedAccessException can occur when zip is attempting to overwrite a hidden and/or read-only file. + catch (UnauthorizedAccessException e) + { + if (tryAgain) + { + HandleError(arc, file, report, e, DPArchiveErrorArgs.UnauthorizedAccessAfterExplanation); + return; + } + if (!TryHelper.TryFixFilePermissions(fileInfo ??= FileSystem.CreateFileInfo(expectedPath), out Exception? ex)) + HandleError(arc, file, report, new AggregateException(e, ex), DPArchiveErrorArgs.UnauthorizedAccessExplanation); + // Try it again. + tryAgain = true; + goto EXTRACT; + } + catch (Exception e) + { + HandleError(arc, file, report, e, "Unknown error occurred while extracting file"); + } + } + + private void HandleProgressionZIP(DPFile file, int i, int max) + { + i = Math.Min(i, max); + var percentComplete = (float)i / max; + var progress = (byte)Math.Floor(percentComplete * 100); + DPArchive arc = file.AssociatedArchive!; + EmitOnExtractionProgress(arc, new DPExtractProgressArgs(progress, arc, file)); + } + + private void HandleError(DPArchive arc, DPFile? file, DPExtractionReport? report, Exception? e, string msg) + { + Logger.Error(e, msg); + EmitOnArchiveError(arc, new DPArchiveErrorArgs(arc, e, msg)); + if (file is not null && report is not null) + report.ErroredFiles.TryAdd(file, msg); + } + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Factories/ProcessFactory.cs b/src/DAZ_Installer.Core/Extraction/Factories/ProcessFactory.cs new file mode 100644 index 0000000..1d044d7 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Factories/ProcessFactory.cs @@ -0,0 +1,7 @@ +namespace DAZ_Installer.Core.Extraction +{ + internal class ProcessFactory : IProcessFactory + { + public IProcess Create() => new ProcessWrapper(); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Factories/RARFactory.cs b/src/DAZ_Installer.Core/Extraction/Factories/RARFactory.cs new file mode 100644 index 0000000..e712940 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Factories/RARFactory.cs @@ -0,0 +1,9 @@ +using DAZ_Installer.External; + +namespace DAZ_Installer.Core.Extraction +{ + internal class RARFactory : IRARFactory + { + public IRAR Create(string arcPath) => new RAR(arcPath); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Factories/ZipArchiveWrapperFactory.cs b/src/DAZ_Installer.Core/Extraction/Factories/ZipArchiveWrapperFactory.cs new file mode 100644 index 0000000..d0cd26c --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Factories/ZipArchiveWrapperFactory.cs @@ -0,0 +1,7 @@ +namespace DAZ_Installer.Core.Extraction +{ + internal class ZipArchiveWrapperFactory : IZipArchiveFactory + { + public IZipArchive Create(Stream stream) => new ZipArchiveWrapper(stream); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Interfaces/IProcess.cs b/src/DAZ_Installer.Core/Extraction/Interfaces/IProcess.cs new file mode 100644 index 0000000..a299661 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Interfaces/IProcess.cs @@ -0,0 +1,42 @@ +using System.Diagnostics; + +namespace DAZ_Installer.Core.Extraction +{ + /// + /// Interface for the class. + /// + internal interface IProcess : IDisposable + { + /// + StreamWriter StandardInput { get; } + /// + ProcessStartInfo StartInfo { get; set; } + /// + /// Occurs each time an application writes a line to its redirected stream.
+ /// /// Returns the line written. Returns if stream ended. + ///
+ event Action? OutputDataReceived; + /// + /// Occurs each time an application writes a line to its redirected stream.
+ /// Returns the line written. Returns if stream ended. + ///
+ event Action? ErrorDataReceived; + /// + event Action Exited; + /// + bool HasExited { get; } + /// + bool EnableRaisingEvents { get; set; } + /// + void Start(); + /// + void Kill(bool entireProcessTree); + /// + void BeginOutputReadLine(); + /// + void BeginErrorReadLine(); + /// + bool WaitForExit(int milliseconds); + + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Interfaces/IProcessFactory.cs b/src/DAZ_Installer.Core/Extraction/Interfaces/IProcessFactory.cs new file mode 100644 index 0000000..fe44644 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Interfaces/IProcessFactory.cs @@ -0,0 +1,7 @@ +namespace DAZ_Installer.Core.Extraction +{ + internal interface IProcessFactory + { + IProcess Create(); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Interfaces/IRAR.cs b/src/DAZ_Installer.Core/Extraction/Interfaces/IRAR.cs new file mode 100644 index 0000000..89ad088 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Interfaces/IRAR.cs @@ -0,0 +1,39 @@ +using DAZ_Installer.External; +using static DAZ_Installer.External.RAR; + +namespace DAZ_Installer.Core.Extraction +{ + /// + /// An interface for the class. + /// + public interface IRAR : IDisposable + { + /// + event MissingVolumeHandler MissingVolume; + /// + event NewFileHandler NewFile; + /// + event PasswordRequiredHandler PasswordRequired; + /// + event ExtractionProgressHandler ExtractionProgress; + + /// + RARFileInfo CurrentFile { get; } + /// + RAR.RAROpenArchiveDataEx ArchiveData { get; } + /// + string DestinationPath { get; set; } + /// + void Close(); + /// + void Open(RAR.OpenMode mode); + /// + bool ReadHeader(); + /// + void Skip(); + /// + void Test(); + /// + void Extract(string destinationName); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Interfaces/IRARFactory.cs b/src/DAZ_Installer.Core/Extraction/Interfaces/IRARFactory.cs new file mode 100644 index 0000000..b056447 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Interfaces/IRARFactory.cs @@ -0,0 +1,7 @@ +namespace DAZ_Installer.Core.Extraction +{ + internal interface IRARFactory + { + IRAR Create(string arcPath); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchive.cs b/src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchive.cs new file mode 100644 index 0000000..6abd4da --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchive.cs @@ -0,0 +1,17 @@ +using System.IO.Compression; + +namespace DAZ_Installer.Core.Extraction +{ + /// + internal interface IZipArchive : IDisposable + { + /// + IReadOnlyCollection Entries { get; } + /// + ZipArchiveMode Mode { get; } + /// + IZipArchiveEntry CreateEntry(string entryName); + /// + IZipArchiveEntry? GetEntry(string entryName); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchiveEntry.cs b/src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchiveEntry.cs new file mode 100644 index 0000000..88ec251 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchiveEntry.cs @@ -0,0 +1,29 @@ +using System.IO.Compression; + +namespace DAZ_Installer.Core.Extraction +{ + /// + internal interface IZipArchiveEntry + { + /// + IZipArchive Archive { get; } + /// + string Name { get; } + /// + string FullName { get; } + /// + long Length { get; } + /// + long CompressedLength { get; } + /// + DateTimeOffset LastWriteTime { get; set; } + /// + void Delete(); + /// + void ExtractToFile(string destinationFileName); + /// + void ExtractToFile(string destinationFileName, bool overwrite); + /// + Stream Open(); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchiveFactory.cs b/src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchiveFactory.cs new file mode 100644 index 0000000..c46a1ee --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchiveFactory.cs @@ -0,0 +1,7 @@ +namespace DAZ_Installer.Core.Extraction +{ + internal interface IZipArchiveFactory + { + IZipArchive Create(Stream stream); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Wrappers/ProcessWrapper.cs b/src/DAZ_Installer.Core/Extraction/Wrappers/ProcessWrapper.cs new file mode 100644 index 0000000..60d5fdf --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Wrappers/ProcessWrapper.cs @@ -0,0 +1,45 @@ +using System.Diagnostics; + +namespace DAZ_Installer.Core.Extraction +{ + /// + /// An implementation of that wraps the class. + /// + internal class ProcessWrapper : IProcess + { + private readonly Process process = new(); + public event Action? OutputDataReceived + { + add => process.OutputDataReceived += (_, e) => value?.Invoke(e.Data); + remove => process.OutputDataReceived -= (_, e) => value?.Invoke(e.Data); + } + public event Action? ErrorDataReceived + { + add => process.ErrorDataReceived += (_, e) => value?.Invoke(e.Data); + remove => process.ErrorDataReceived -= (_, e) => value?.Invoke(e.Data); + } + public event Action Exited + { + add => process.Exited += (_, __) => value?.Invoke(); + remove => process.Exited -= (_, __) => value?.Invoke(); + } + public StreamWriter StandardInput => process.StandardInput; + public bool HasExited => process.HasExited; + public bool EnableRaisingEvents { get => process.EnableRaisingEvents; set => process.EnableRaisingEvents = value; } + public ProcessStartInfo StartInfo { get => process.StartInfo; set => process.StartInfo = value; } + + internal ProcessWrapper() { } + + public void BeginErrorReadLine() => process.BeginErrorReadLine(); + public void BeginOutputReadLine() => process.BeginOutputReadLine(); + public void Kill(bool entireProcessTree) => process.Kill(entireProcessTree); + public void Start() => process.Start(); + public bool WaitForExit(int milliseconds) => process.WaitForExit(milliseconds); + + public void Dispose() + { + process.Dispose(); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Wrappers/ZipArchiveEntryWrapper.cs b/src/DAZ_Installer.Core/Extraction/Wrappers/ZipArchiveEntryWrapper.cs new file mode 100644 index 0000000..3caf9c1 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Wrappers/ZipArchiveEntryWrapper.cs @@ -0,0 +1,36 @@ +using System.IO.Compression; + +namespace DAZ_Installer.Core.Extraction +{ + /// + /// Wrapper for + /// + internal class ZipArchiveEntryWrapper : IZipArchiveEntry + { + private readonly ZipArchiveEntry _entry; + + internal ZipArchiveEntryWrapper(ZipArchiveEntry entry) => _entry = entry; + /// + public string Name => _entry.Name; + /// + public string FullName => _entry.FullName; + /// + public long Length => _entry.Length; + /// + public DateTimeOffset LastWriteTime { get => _entry.LastWriteTime; set => _entry.LastWriteTime = value; } + /// + public IZipArchive Archive => new ZipArchiveWrapper(_entry.Archive); + /// + public long CompressedLength => _entry.CompressedLength; + /// + public void Delete() => _entry.Delete(); + /// + public void ExtractToFile(string destinationFileName) => _entry.ExtractToFile(destinationFileName); + /// + public void ExtractToFile(string destinationFileName, bool overwrite) => _entry.ExtractToFile(destinationFileName, overwrite); + /// + public Stream Open() => _entry.Open(); + /// + public override string ToString() => _entry.ToString(); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Wrappers/ZipArchiveWrapper.cs b/src/DAZ_Installer.Core/Extraction/Wrappers/ZipArchiveWrapper.cs new file mode 100644 index 0000000..d671398 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Wrappers/ZipArchiveWrapper.cs @@ -0,0 +1,32 @@ +using System.IO.Compression; + +namespace DAZ_Installer.Core.Extraction +{ + /// + /// Wrapper for + /// + internal class ZipArchiveWrapper : IZipArchive + { + readonly ZipArchive archive; + + internal ZipArchiveWrapper(ZipArchive archive) => this.archive = archive; + + public ZipArchiveWrapper(Stream stream) => archive = new(stream); + public IReadOnlyCollection Entries => archive.Entries.Select(x => new ZipArchiveEntryWrapper(x)).ToList(); + + public ZipArchiveMode Mode => archive.Mode; + + public IZipArchiveEntry CreateEntry(string entryName) => new ZipArchiveEntryWrapper(archive.CreateEntry(entryName)); + public void Dispose() + { + archive.Dispose(); + GC.SuppressFinalize(this); + } + + public IZipArchiveEntry? GetEntry(string entryName) + { + var entry = archive.GetEntry(entryName); + return entry is null ? null : new ZipArchiveEntryWrapper(entry); + } + } +} diff --git a/src/Libs/UnRAR.dll b/src/DAZ_Installer.Core/Libs/UnRAR.dll similarity index 100% rename from src/Libs/UnRAR.dll rename to src/DAZ_Installer.Core/Libs/UnRAR.dll diff --git a/src/DAZ_Installer.Core/Properties/AssemblyInfo1.cs b/src/DAZ_Installer.Core/Properties/AssemblyInfo1.cs new file mode 100644 index 0000000..16dc5f9 --- /dev/null +++ b/src/DAZ_Installer.Core/Properties/AssemblyInfo1.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// In SDK-style projects such as this one, several assembly attributes that were historically +// defined in this file are now automatically added during build and populated with +// values defined in project properties. For details of which attributes are included +// and how to customise this process see: https://aka.ms/assembly-info-properties + + +// Setting ComVisible to false makes the types in this assembly not visible to COM +// components. If you need to access a type in this assembly from COM, set the ComVisible +// attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM. + +[assembly: Guid("b8a29ead-4696-4dde-bdd7-0aa54fa23802")] +[assembly: InternalsVisibleTo("DAZ_Installer.CoreTests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // For generating mocks with NSubstitute diff --git a/src/DAZ_Installer.CoreTests/DAZ_Installer.CoreTests.csproj b/src/DAZ_Installer.CoreTests/DAZ_Installer.CoreTests.csproj new file mode 100644 index 0000000..9ad68b4 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/DAZ_Installer.CoreTests.csproj @@ -0,0 +1,59 @@ + + + + net6.0 + enable + enable + + false + true + x64 + + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + + + + + + + + + diff --git a/src/DAZ_Installer.CoreTests/DPDSXFileTests.cs b/src/DAZ_Installer.CoreTests/DPDSXFileTests.cs new file mode 100644 index 0000000..1a1a675 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/DPDSXFileTests.cs @@ -0,0 +1,77 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using DAZ_Installer.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DAZ_Installer.IO.Fakes; + +namespace DAZ_Installer.Core.Tests +{ + [TestClass] + public class DPDSXFileTests + { + internal const string SupplementContent = + @" + + + + "; + + internal const string SupportContent = + @" + + + + + + + + + + + + + "; + + StreamReader SetupStreamReader(string str) + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(str); + writer.Flush(); + stream.Position = 0; + return new StreamReader(stream); + } + + [TestMethod] + public void CheckContentsTest_Supplement() + { + var f = new DPDSXFile("doesnt matter", new(), null); + var a = f.AssociatedArchive; + using var sr = SetupStreamReader(SupplementContent); + + f.CheckContents(sr); + + Assert.AreEqual("Test Product", a.ProductInfo.ProductName); + Assert.AreEqual("Test Product", a.ProductName); + } + + [TestMethod] + public void CheckContentsTest_Support() + { + var f = new DPDSXFile("doesnt matter", new(), null); + var a = f.AssociatedArchive; + using var sr = SetupStreamReader(SupportContent); + + f.CheckContents(sr); + + Assert.AreEqual("82114", f.ContentInfo.ID); + Assert.AreEqual("82114", a.ProductInfo.SKU); + CollectionAssert.AreEqual(new[] { "TheRealSolly", "Sollybean" }, a.ProductInfo.Authors.ToArray()); + CollectionAssert.AreEqual(new[] { "TheRealSolly", "Sollybean" }, f.ContentInfo.Authors.ToArray()); + + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/DPDazFileTests.cs b/src/DAZ_Installer.CoreTests/DPDazFileTests.cs new file mode 100644 index 0000000..de988a2 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/DPDazFileTests.cs @@ -0,0 +1,61 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Serilog; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; + +namespace DAZ_Installer.Core.Tests +{ + [TestClass] + public class DPDazFileTests + { + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Information() + .CreateLogger(); + } + + StreamReader SetupStreamReader(string str) + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(str); + writer.Flush(); + stream.Position = 0; + return new StreamReader(stream); + } + [TestMethod] + public void ReadContentsTest() + { + // Arrange + var f = new DPDazFile("doesnt matter", new DPArchive(), null); + const string DSFContents = + @"{ + ""file_version"" : ""0.6.0.0"", + ""asset_info"" : { + ""id"" : ""/data/data.dsf"", + ""type"" : ""prop"", + ""contributor"" : { + ""author"" : ""TheRealSolly"", + ""email"" : ""solomon1blount@gmail.com"", + ""website"" : ""www.thesolomonchronicles.com"" + }, + ""revision"" : ""1.0"", + ""modified"" : ""2020-12-06T00:04:11Z"" + } + }"; + + // Act + using var sr = SetupStreamReader(DSFContents); + f.ReadContents(sr); + + // Assert + Assert.AreEqual("TheRealSolly", f.ContentInfo.Authors[0]); + Assert.AreEqual(ContentType.Prop, f.ContentInfo.ContentType); + Assert.AreEqual("www.thesolomonchronicles.com", f.ContentInfo.Website); + Assert.AreEqual("solomon1blount@gmail.com", f.ContentInfo.Email); + } + } +} diff --git a/src/DAZ_Installer.CoreTests/DPDestinationDeterminerTests.cs b/src/DAZ_Installer.CoreTests/DPDestinationDeterminerTests.cs new file mode 100644 index 0000000..4e7f170 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/DPDestinationDeterminerTests.cs @@ -0,0 +1,434 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using DAZ_Installer.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Moq; +using DAZ_Installer.IO.Fakes; +using DAZ_Installer.Core.Extraction.Fakes; +using DAZ_Installer.Core.Extraction; +using DAZ_Installer.IO; +using DAZ_Installer.CoreTests.Extraction; +using Serilog; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using static DAZ_Installer.Core.Tests.Helpers.DPDestinationDeterminerTestHelpers; +using System.Reflection; +using System.IO; +using System.Xml.Linq; + +namespace DAZ_Installer.Core.Tests +{ + [TestClass] + public class DPDestinationDeterminerTests + { + // Supplement and Manifest should not be included. + public static readonly string[] DefaultManifestPaths = new[] { "Manifest.dsx", "Supplement.dsx", "Content/data/TheReaolSolly/a.txt", "Content/docs/TheRealSolly/b.txt" }; + public static readonly string[] DefaultNonDazPaths = new[] { "data/TheRealSolly/a.txt", "docs/TheRealSolly/b.txt", "should not be included.txt" }; + private static readonly MockOptions DazArchiveOpts = new(); + private static readonly MockOptions NonDazOpts = new() { paths = DefaultNonDazPaths }; + private static readonly DPProcessSettings ManifestProcessSettings = new("Z:/", + "Z:/", + InstallOptions.ManifestOnly, + DPProcessor.DefaultContentFolders.ToHashSet(), + new Dictionary(DPProcessor.DefaultRedirects)); + private static readonly DPProcessSettings AutoProcessSettings = ManifestProcessSettings with { InstallOption = InstallOptions.Automatic }; + private static readonly DPProcessSettings BothProcessSettings = ManifestProcessSettings with { InstallOption = InstallOptions.ManifestAndAuto }; + + // TODO: Test DPDestinationDeterminer when a folder is not included in the content redirect folders map. + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Information() + .CreateLogger(); + } + public struct MockOptions + { + public bool partialFileInfo = true; + public bool partialDPFileInfo = true; + public bool partialZipArchiveEntry = true; + public bool partialFileSystem = true; + public string[] paths = DefaultManifestPaths; + + public MockOptions() { } + } + + private static DPArchive NewMockedArchive(MockOptions options, out Mock fakeDPFileInfo, out Mock fakeFileInfo, out Mock fakeFileSystem) + { + var fs = new Mock(DPFileScopeSettings.All) { CallBase = options.partialFileSystem }; + fs.Object.PartialMock = options.partialFileSystem; + fakeFileSystem = fs; + fakeFileInfo = new Mock("Z:/test.zip") { CallBase = options.partialFileInfo }; + fakeDPFileInfo = new Mock(fakeFileInfo.Object, fs.Object, null) { CallBase = options.partialFileInfo }; + var arc = new DPArchive(string.Empty, Log.Logger.ForContext(), fakeDPFileInfo.Object, Mock.Of()); + foreach (var file in options.paths) + { + var dpFile = DPFile.CreateNewFile(file, arc, null); + var fi = fakeFileSystem.Object.CreateFileInfo(file); + dpFile.FileInfo = fi; + Exception? ex = null; + Mock.Get(fi).Setup(x => x.TryAndFixOpenRead(out It.Ref.IsAny, out ex)) + .Callback((out Stream s, out Exception ex) => + { + s = DPArchiveTestHelpers.DetermineFileStream(dpFile, arc, arc.Contents.Values.Where(x => x.Parent is not null).Select(x => x.Path)); + ex = null; + }) + .Returns(true); + } + return arc; + } + + [TestMethod] + public void DetermineDestinationsTest_ManifestDAZArchive() + { + var tc = new { + Paths = DefaultManifestPaths, + Settings = ManifestProcessSettings, + Options = DazArchiveOpts, + ExpectedTargetPaths = new[] { "Z:/data/TheReaolSolly/a.txt", "Z:/Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathsTargetPaths = new[] { "data/TheReaolSolly/a.txt", "Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathToContentFolders = new[] { "data/TheReaolSolly/a.txt", "docs/TheRealSolly/b.txt" }, + ExpectedContentFoldersMarked = new[] { "Content/data", "Content/docs" }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(arc.Contents.Values.Where(x => x.Parent is not null)); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_ManifestNonDAZArchive() + { + var tc = new + { + Paths = DefaultNonDazPaths, + Settings = ManifestProcessSettings, + Options = NonDazOpts, + ExpectedTargetPaths = new string[] { }, + ExpectedRelativePathsTargetPaths = new string[] { }, + ExpectedRelativePathToContentFolders = new string[] { }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + var expected = new HashSet(0); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + } + + [TestMethod] + public void DetermineDestinationsTest_AutoDAZArchive() + { + var tc = new + { + Paths = DefaultManifestPaths, + Settings = AutoProcessSettings, + Options = DazArchiveOpts, + ExpectedTargetPaths = new[] { "Z:/data/TheReaolSolly/a.txt", "Z:/Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathsTargetPaths = new[] { "data/TheReaolSolly/a.txt", "Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathToContentFolders = new[] { "data/TheReaolSolly/a.txt", "docs/TheRealSolly/b.txt" }, + ExpectedContentFoldersMarked = new[] { "Content/data", "Content/docs" }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(arc.Contents.Values.Where(x => x.Parent is not null)); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_AutoNonDAZArchive() + { + var tc = new + { + Paths = DefaultNonDazPaths, + Settings = AutoProcessSettings, + Options = NonDazOpts, + ExpectedTargetPaths = new[] { "Z:/data/TheRealSolly/a.txt", "Z:/Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathsTargetPaths = new[] { "data/TheRealSolly/a.txt", "Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathToContentFolders = new[] { "data/TheRealSolly/a.txt", "docs/TheRealSolly/b.txt" }, + ExpectedContentFoldersMarked = new[] { "data", "docs" }, + }; + + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(arc.Contents.Values.Where(x => x.Parent is not null)); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_BothDAZArchive() + { + var tc = new + { + Paths = DefaultManifestPaths, + Settings = BothProcessSettings, + Options = DazArchiveOpts, + ExpectedTargetPaths = new[] { "Z:/data/TheReaolSolly/a.txt", "Z:/Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathsTargetPaths = new[] { "data/TheReaolSolly/a.txt", "Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathToContentFolders = new[] { "data/TheReaolSolly/a.txt", "docs/TheRealSolly/b.txt" }, + ExpectedContentFoldersMarked = new[] { "Content/data", "Content/docs" }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(arc.Contents.Values.Where(x => x.Parent is not null)); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_BothNonDAZArchive() + { + var tc = new + { + Paths = DefaultNonDazPaths, + Settings = BothProcessSettings, + Options = NonDazOpts, + ExpectedTargetPaths = new[] { "Z:/data/TheRealSolly/a.txt", "Z:/Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathsTargetPaths = new[] { "data/TheRealSolly/a.txt", "Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathToContentFolders = new[] { "data/TheRealSolly/a.txt", "docs/TheRealSolly/b.txt" }, + ExpectedContentFoldersMarked = new[] { "data", "docs" }, + }; + + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(arc.Contents.Values.Where(x => x.Parent is not null)); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_NoRedirectsDAZManifest() + { + var tc = new + { + Paths = DefaultManifestPaths, + Settings = ManifestProcessSettings with { ContentRedirectFolders = new() }, + Options = DazArchiveOpts, + ExpectedTargetPaths = new[] { "Z:/data/TheReaolSolly/a.txt", "Z:/docs/TheRealSolly/b.txt" }, + ExpectedRelativePathsTargetPaths = new[] { "data/TheReaolSolly/a.txt", "docs/TheRealSolly/b.txt" }, + ExpectedRelativePathToContentFolders = new[] { "data/TheReaolSolly/a.txt", "docs/TheRealSolly/b.txt" }, + ExpectedContentFoldersMarked = new[] { "Content/data", "Content/docs" }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(arc.Contents.Values.Where(x => x.Parent is not null)); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_NoRedirectsDAZManifestAuto() + { + var tc = new + { + Paths = DefaultManifestPaths, + Settings = AutoProcessSettings with { ContentRedirectFolders = new() }, + Options = DazArchiveOpts, + ExpectedTargetPaths = new[] { "Z:/data/TheReaolSolly/a.txt" }, + ExpectedRelativePathsTargetPaths = new[] { "data/TheReaolSolly/a.txt" }, + ExpectedRelativePathToContentFolders = new[] { "data/TheReaolSolly/a.txt" }, + ExpectedContentFoldersMarked = new[] { "Content/data" }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet( new[] { arc.Contents[PathHelper.NormalizePath("Content/data/TheReaolSolly/a.txt")] }); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_NoRedirectsDAZManifestBoth() + { + var tc = new + { + Paths = DefaultManifestPaths, + Settings = BothProcessSettings with { ContentRedirectFolders = new() }, + Options = DazArchiveOpts, + ExpectedTargetPaths = new[] { "Z:/data/TheReaolSolly/a.txt", "Z:/docs/TheRealSolly/b.txt" }, + ExpectedRelativePathsTargetPaths = new[] { "data/TheReaolSolly/a.txt", "docs/TheRealSolly/b.txt" }, + ExpectedRelativePathToContentFolders = new[] { "data/TheReaolSolly/a.txt", "docs/TheRealSolly/b.txt" }, + ExpectedContentFoldersMarked = new[] { "Content/data", "Content/docs" }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(arc.Contents.Values.Where(x => x.Parent is not null)); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_ContentFolderTree() + { + var paths = new string[] { "data/data/a.txt", "docs/docs/b.txt", "data/data/docs/docs/c.txt" }; + var tc = new + { + Paths = paths, + Settings = AutoProcessSettings, + Options = new MockOptions() { paths = paths }, + ExpectedTargetPaths = new[] { "Z:/data/data/a.txt", "Z:/Documentation/docs/b.txt", "Z:/data/data/docs/docs/c.txt" }, + ExpectedRelativePathsTargetPaths = new[] { "data/data/a.txt", "Documentation/docs/b.txt", "data/data/docs/docs/c.txt" }, + ExpectedRelativePathToContentFolders = new[] { "data/data/a.txt", "docs/docs/b.txt", "data/data/docs/docs/c.txt" }, + ExpectedContentFoldersMarked = new[] { "data", "docs" }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(arc.Contents.Values.Where(x => x.Parent is not null)); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_NoDirs() + { + var paths = new string[] { "a.txt", "b.txt", "c.txt" }; + var tc = new + { + Paths = paths, + Settings = AutoProcessSettings, + Options = new MockOptions() { paths = paths }, + ExpectedTargetPaths = new string[] { }, + ExpectedRelativePathsTargetPaths = new string[] { }, + ExpectedRelativePathToContentFolders = new string[] { }, + ExpectedContentFoldersMarked = new string[] { }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(0); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_NoDirsEmptyContentFolderSet() + { + var paths = new string[] { "a.txt", "b.txt", "c.txt" }; + var tc = new + { + Paths = paths, + Settings = AutoProcessSettings with { ContentFolders = new HashSet() { "" } }, + Options = new MockOptions() { paths = paths }, + ExpectedTargetPaths = new string[] { }, + ExpectedRelativePathsTargetPaths = new string[] { }, + ExpectedRelativePathToContentFolders = new string[] { }, + ExpectedContentFoldersMarked = new string[] { }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(0); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/DPProcessorTests.cs b/src/DAZ_Installer.CoreTests/DPProcessorTests.cs new file mode 100644 index 0000000..67aa50f --- /dev/null +++ b/src/DAZ_Installer.CoreTests/DPProcessorTests.cs @@ -0,0 +1,271 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using Serilog; +using DAZ_Installer.Core.Extraction; +using Moq; +using DAZ_Installer.IO.Fakes; +using DAZ_Installer.IO; + +#pragma warning disable 618 +namespace DAZ_Installer.Core.Tests +{ + [TestClass] + public class DPProcessorTests + { + static IEnumerable DefaultContents => DPProcessorTestHelpers.DefaultContents; + static readonly DPProcessSettings DefaultProcessSettings = new("A:/", "B:/", InstallOptions.ManifestAndAuto); + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Information() + .CreateLogger(); + } + + [TestMethod] + public void ProcessArchiveTest() + { + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions, out var _, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var settings = DPProcessorTestHelpers.CreateExtractSettings(DefaultContents, a); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectArchiveProcessed = new() { { a.FileName, DPProcessorTestHelpers.CreateExtractionReport(settings, Enumerable.Empty(), DPProcessorTestHelpers.CalculateExpectedFiles(DefaultContents)) } } + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + + p.ProcessArchive(a, DefaultProcessSettings); + + DPProcessorTestHelpers.AssertCommon(p); + //CollectionAssert.Contains(a.ProductInfo.Tags.ToArray(), new[] { "Gentlemen's Library", "TheRealSolly", "solomon1blount@gmail.com", "www.thesolomonchronicles.com", a.FileName }); + } + + [TestMethod] + public void ProcessArchiveTest_AfterCancelledProcess() + { + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions, out var _, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var settings = DPProcessorTestHelpers.CreateExtractSettings(DefaultContents, a); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectArchiveProcessed = new() { { a.FileName, DPProcessorTestHelpers.CreateExtractionReport(settings, Enumerable.Empty(), DPProcessorTestHelpers.CalculateExpectedFiles(DefaultContents)) } }, + ExpectedArchiveCount = 2, + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + var cancelledOnce = false; + p.ArchiveEnter += (_, __) => + { + if (!cancelledOnce) p.CancelProcessing(); + cancelledOnce = true; + }; + + p.ProcessArchive(a, DefaultProcessSettings); + p.ProcessArchive(a, DefaultProcessSettings); + + + DPProcessorTestHelpers.AssertCommon(p, Times.AtLeast(1)); + //CollectionAssert.Contains(a.ProductInfo.Tags.ToArray(), new[] { "Gentlemen's Library", "TheRealSolly", "solomon1blount@gmail.com", "www.thesolomonchronicles.com", a.FileName }); + } + + [TestMethod] + public void ProcessArchiveTest_AfterProcess() + { + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions, out var _, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var settings = DPProcessorTestHelpers.CreateExtractSettings(DefaultContents, a); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectArchiveProcessed = new() { { a.FileName, DPProcessorTestHelpers.CreateExtractionReport(settings, Enumerable.Empty(), DPProcessorTestHelpers.CalculateExpectedFiles(DefaultContents)) } }, + ExpectedArchiveCount = 2, + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + + p.ProcessArchive(a, DefaultProcessSettings); + p.ProcessArchive(a, DefaultProcessSettings); + + + DPProcessorTestHelpers.AssertCommon(p, Times.Exactly(2)); + //CollectionAssert.Contains(a.ProductInfo.Tags.ToArray(), new[] { "Gentlemen's Library", "TheRealSolly", "solomon1blount@gmail.com", "www.thesolomonchronicles.com", a.FileName }); + } + + [TestMethod] + public void ProcessArchiveTest_AfterProcessError() + { + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions, out var _, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var settings = DPProcessorTestHelpers.CreateExtractSettings(DefaultContents, a); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectArchiveProcessed = new() { { a.FileName, DPProcessorTestHelpers.CreateExtractionReport(settings, Enumerable.Empty(), DPProcessorTestHelpers.CalculateExpectedFiles(DefaultContents)) } }, + ExpectedArchiveCount = 2, + ExpectedProcessErrorCount = 1, + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + + var calledOnce = false; + var mock = new Mock(new FakeFileSystem(), "A:/") { CallBase = true }.Object; + fs.Setup(x => x.CreateDriveInfo(It.IsRegex(@"A:/"))).Returns(() => + { + if (!calledOnce) + { + calledOnce = true; + throw new Exception("CreateDrive-a-doo"); + } + return mock; + }); + p.ProcessArchive(a, DefaultProcessSettings); + p.ProcessArchive(a, DefaultProcessSettings); + + + DPProcessorTestHelpers.AssertCommon(p, Times.Once()); + //CollectionAssert.Contains(a.ProductInfo.Tags.ToArray(), new[] { "Gentlemen's Library", "TheRealSolly", "solomon1blount@gmail.com", "www.thesolomonchronicles.com", a.FileName }); + } + + [TestMethod] + [DataRow("null", "null")] + [DataRow("null", "A:/")] + [DataRow("", "A:/")] + [DataRow("", "null")] + [DataRow("A:/", "null")] + + public void ProcessArchiveTest_InvalidProcessSettings(string? temp, string? dest) + { + if (temp == "null") temp = null; + if (dest == "null") dest = null; + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions, out var _, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var settings = DPProcessorTestHelpers.CreateExtractSettings(DefaultContents, a); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectArchiveProcessed = new() { { a.FileName, DPProcessorTestHelpers.CreateExtractionReport(settings, Enumerable.Empty(), DPProcessorTestHelpers.CalculateExpectedFiles(DefaultContents)) } } + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + + try + { + p.ProcessArchive(a, new DPProcessSettings(temp, dest, InstallOptions.Automatic)); + Assert.Fail("Expected exception, got none."); + } catch { } + } + + [TestMethod] + public void ProcessArchiveTest_PeekError() + { + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions, out var e, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectedProcessErrorCount = 1, + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + e.Setup(x => x.Peek(It.IsAny())).Throws(new Exception("Peek-a-boo")); + p.ArchiveExit += (_, p) => Assert.IsFalse(p.Processed); + p.ProcessArchive(a, DefaultProcessSettings); + } + [TestMethod] + public void ProcessArchiveTest_ExtractError() + { + Func extractFunc = () => throw new Exception("Extract-a-doo"); + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions with { ExtractFunc = extractFunc }, out var e, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectedProcessErrorCount = 1, + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + p.ArchiveExit += (_, p) => Assert.IsFalse(p.Processed); + p.ProcessArchive(a, DefaultProcessSettings); + } + [TestMethod] + public void ProcessArchiveTest_ExtractToTempError() + { + Func extractFunc = () => throw new Exception("Extract-a-doo"); + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions with { ExtractToTempFunc = extractFunc }, out var e, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectedProcessErrorCount = 1, + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + p.ArchiveExit += (_, p) => Assert.IsFalse(p.Processed); + p.ProcessArchive(a, DefaultProcessSettings); + } + [TestMethod] + public void ProcessArchiveTest_OutOfStorage() + { + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions, out var e, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectedProcessErrorCount = 1, + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + p.ArchiveExit += (_, p) => Assert.IsFalse(p.Processed); + var fakeDriveInfo = new Mock(fs.Object, "N:/"); + fs.Setup(x => x.CreateDriveInfo(It.IsAny())).Returns(fakeDriveInfo.Object); + fakeDriveInfo.Object.AvailableFreeSpace = 0; + p.ProcessError += (_, __) => p.CancelCurrentArchive(); + p.ProcessArchive(a, DefaultProcessSettings); + } + + [TestMethod] + public void ProcessArchiveTest_OutOfStorageFixedButExtractError() + { + Func extractErrorFunc = () => throw new Exception("no u"); + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions with { ExtractFunc = extractErrorFunc }, out var e, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectedProcessErrorCount = 1, + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + p.ArchiveExit += (_, p) => Assert.IsFalse(p.Processed); + var fakeDriveInfo = new Mock(fs.Object, "N:/"); + fakeDriveInfo.Object.AvailableFreeSpace = 0; + fs.Setup(x => x.CreateDriveInfo(It.IsAny())).Returns(() => + { + if (fakeDriveInfo.Object.AvailableFreeSpace == 0) fakeDriveInfo.SetupProperty(x => x.AvailableFreeSpace, long.MaxValue); + return fakeDriveInfo.Object; + }); + p.ProcessArchive(a, DefaultProcessSettings); + } + + [TestMethod] + public void ProcessArchiveOrderTest() + { + var opts = DPProcessorTestHelpers.DefaultMockOptions with { paths = Enumerable.Empty() }; + var a = DPProcessorTestHelpers.NewMockedArchive(opts, out var _, out _, out var ff, out var fs); + a.Path = ff.Object.FullName = "a.rar"; + var b = DPProcessorTestHelpers.NewMockedArchive(opts, out var _, out _, out ff, out _); + b.Path = ff.Object.FullName = "b.rar"; + var c = DPProcessorTestHelpers.NewMockedArchive(opts, out var _, out _, out ff, out _); + c.Path = ff.Object.FullName = "c.rar"; + var d = DPProcessorTestHelpers.NewMockedArchive(opts, out var _, out _, out ff, out _); + d.Path = ff.Object.FullName = "d.rar"; + var e = DPProcessorTestHelpers.NewMockedArchive(opts, out var _, out _, out ff, out _); + e.Path = ff.Object.FullName = "e.rar"; + a.Subarchives.AddRange(new[] { b, c }); + b.Subarchives.Add(d); + c.Subarchives.Add(e); + + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out var _, out var _); + var settings = DPProcessorTestHelpers.CreateExtractSettings(Enumerable.Empty(), a); + Queue expectedOrder = new(new[] { a, c, e, b, d }); + Queue expectedOrderClone = new(expectedOrder); + + p.ArchiveEnter += (_, p) => Assert.AreEqual(p.Archive, expectedOrder.Dequeue()); + p.ArchiveExit += (_, p) => Assert.AreEqual(p.Archive, expectedOrderClone.Dequeue()); + + p.ProcessArchive(a, DefaultProcessSettings); + + Assert.IsTrue(expectedOrder.Count == 0); + Assert.IsTrue(expectedOrderClone.Count == 0); + } + + // Invalid Process Settings + // + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/DPTagProviderTests.cs b/src/DAZ_Installer.CoreTests/DPTagProviderTests.cs new file mode 100644 index 0000000..6986671 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/DPTagProviderTests.cs @@ -0,0 +1,188 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using DAZ_Installer.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; +using Serilog; +using DAZ_Installer.Core.Extraction; +using DAZ_Installer.IO.Fakes; +using Moq; +using DAZ_Installer.CoreTests.Extraction; + +namespace DAZ_Installer.Core.Tests +{ + [TestClass] + public class DPTagProviderTests + { + public static IEnumerable DefaultContents => new string[] { "Manifest.dsx", "Supplement.dsx", "Contents/a.txt", "Contents/b.txt", "Contents/Documents/c.png", "Contents/Documents/d.txt", "Contents/e.duf", "Contents/f.duf", "bullshit.png" }; + public static MockOptions DefaultMockOptions => new(); + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Information() + .CreateLogger(); + } + + public struct MockOptions + { + public bool partialFileInfo = true; + public bool partialRAR = true; + public bool partialDPFileInfo = true; + public bool partialZipArchiveEntry = true; + public bool partialFakeFileSystem = true; + public IEnumerable paths = DefaultContents; + public Func? ExtractToTempFunc = null; + public Func? ExtractFunc = null; + + public MockOptions() { } + } + public static DPArchive NewMockedArchive(MockOptions options, out Mock extractor, out Mock fakeDPFileInfo, out Mock fakeFileInfo, out Mock fakeFileSystem) + { + var fs = new Mock() { CallBase = options.partialFakeFileSystem }; + fakeFileSystem = fs; + fakeFileInfo = new Mock("Z:/test.rar") { CallBase = options.partialFileInfo }; + fakeDPFileInfo = new Mock(fakeFileInfo.Object, fakeFileSystem.Object, null) { CallBase = options.partialDPFileInfo }; + extractor = new Mock(); + var arc = new DPArchive(string.Empty, Log.Logger.ForContext(), fakeDPFileInfo.Object, extractor.Object); + extractor.Setup(x => x.ExtractToTemp(It.IsAny())).Returns((DPExtractSettings x) => + { + return options.ExtractToTempFunc is null ? handleExtract(x, fs.Object) : options.ExtractToTempFunc(); + }); + extractor.Setup(x => x.Extract(It.IsAny())).Returns((DPExtractSettings x) => + { + return options.ExtractFunc is null ? handleExtract(x, fs.Object) : options.ExtractFunc(); + }); + SetupEntities(options.paths, arc); + extractor.Object.Extract(new DPExtractSettings("Z:/", Enumerable.Empty(), true, arc)); + return arc; + } + + private static void SetupEntities(IEnumerable paths, DPArchive arc) + { + foreach (var path in paths) + { + if (string.IsNullOrEmpty(Path.GetFileName(path))) new DPFolder(path, arc, null); + else DPFile.CreateNewFile(path, arc, null); + } + } + + private static void UpdateFileInfos(DPExtractSettings settings, FakeFileSystem system) + { + foreach (var file in settings.Archive.Contents.Values) + { + var path = string.IsNullOrEmpty(file.TargetPath) ? Path.Combine(settings.TempPath, file.Path) : file.TargetPath; + file.FileInfo = system.CreateFileInfo(path); + var mockFileInfo = Mock.Get(file.FileInfo); + var stream = DPArchiveTestHelpers.DetermineFileStream(file, settings.Archive); + Exception? ex = null; + mockFileInfo.Setup(x => x.TryAndFixOpenRead(out It.Ref.IsAny, out ex)) + .Callback((out Stream s, out Exception ex) => + { + s = DPArchiveTestHelpers.DetermineFileStream(file, settings.Archive); + ex = null; + }) + .Returns(true); + } + } + + private static DPExtractionReport handleExtract(DPExtractSettings settings, FakeFileSystem fs) + { + UpdateFileInfos(settings, fs); + return new DPExtractionReport() + { + ErroredFiles = new(0), + ExtractedFiles = settings.FilesToExtract.ToList(), + Settings = settings + }; + } + + private static void AssertTagsEqual(DPArchive arc, HashSet actual) + { + // Make sure that all of the ProductInfo tags are in the actual tags. + foreach (var tag in arc.DazFiles.Where(x => x.Extracted)) + { + var ci = tag.ContentInfo; + Assert.IsTrue(actual.Contains(ci.ContentType.ToString()), "Content type not found in Tags."); + Assert.IsTrue(actual.Contains(ci.Email), "Email not found in Tags."); + Assert.IsTrue(actual.Contains(ci.Website), "Website not found in Tags."); + foreach (var author in ci.Authors) + Assert.IsTrue(actual.Contains(author), "Author not found in Tags."); + } + foreach (var s in DPArchive.RegexSplitName(arc.ProductInfo.ProductName)) + { + Assert.IsTrue(actual.Contains(s)); + } + } + + [TestMethod] + public void GetTagsTest() + { + var arc = NewMockedArchive(DefaultMockOptions, out var e, out var dpfi, out var fi, out var fs); + var tp = new DPTagProvider(); + + var result = tp.GetTags(arc, new DPProcessSettings("Z:/", "Z:/", InstallOptions.Automatic)); + + AssertTagsEqual(arc, result); + } + + [TestMethod] + public void GetTagsTest_UnderscoreName() + { + var arc = NewMockedArchive(DefaultMockOptions, out var e, out var dpfi, out var fi, out var fs); + fi.Object.FullName = "Z:/i_am_leg.rar"; + var tp = new DPTagProvider(); + + var result = tp.GetTags(arc, new DPProcessSettings("Z:/", "Z:/", InstallOptions.Automatic)); + + AssertTagsEqual(arc, result); + Assert.IsTrue(result.Contains("i") && result.Contains("am") && result.Contains("leg"), "Underlined name not found in Tags."); + } + + [TestMethod] + public void GetTagsTest_DashName() + { + var arc = NewMockedArchive(DefaultMockOptions, out var e, out var dpfi, out var fi, out var fs); + fi.Object.FullName = "Z:/i-am-leg.rar"; + var tp = new DPTagProvider(); + + var result = tp.GetTags(arc, new DPProcessSettings("Z:/", "Z:/", InstallOptions.Automatic)); + + AssertTagsEqual(arc, result); + Assert.IsTrue(result.Contains("i") && result.Contains("am") && result.Contains("leg"), "Underlined name not found in Tags."); + } + + [TestMethod] + public void GetTagsTest_PlusName() + { + var arc = NewMockedArchive(DefaultMockOptions, out var e, out var dpfi, out var fi, out var fs); + fi.Object.FullName = "Z:/i+am+leg.rar"; + var tp = new DPTagProvider(); + + var result = tp.GetTags(arc, new DPProcessSettings("Z:/", "Z:/", InstallOptions.Automatic)); + + AssertTagsEqual(arc, result); + Assert.IsTrue(result.Contains("i") && result.Contains("am") && result.Contains("leg"), "Underlined name not found in Tags."); + } + + [TestMethod] + public void GetTagsTest_MixedName() + { + var arc = NewMockedArchive(DefaultMockOptions, out var e, out var dpfi, out var fi, out var fs); + fi.Object.FullName = "Z:/a+b-c_d.rar"; + var tp = new DPTagProvider(); + + var result = tp.GetTags(arc, new DPProcessSettings("Z:/", "Z:/", InstallOptions.Automatic)); + + AssertTagsEqual(arc, result); + Assert.IsTrue(result.Contains("a") && result.Contains("b") && result.Contains("c") && result.Contains("d"), "Underlined name not found in Tags."); + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/Extraction/DP7zExtractorTests.cs b/src/DAZ_Installer.CoreTests/Extraction/DP7zExtractorTests.cs new file mode 100644 index 0000000..cffbee0 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/DP7zExtractorTests.cs @@ -0,0 +1,540 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using DAZ_Installer.Core.Extraction.Fakes; +using DAZ_Installer.CoreTests.Extraction; +using DAZ_Installer.IO; +using DAZ_Installer.IO.Fakes; +using Moq; +using Serilog; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using Microsoft.VisualStudio.TestPlatform.Utilities; + +#pragma warning disable 618 +namespace DAZ_Installer.Core.Extraction.Tests +{ + [TestClass] + public class DP7zExtractorTests + { + /// + /// A factory that returns a mocked fake archive with the default contents. + /// + static IProcessFactory DefaultFactory { get; set; } = null!; + static string[] DefaultContents => DPArchiveTestHelpers.DefaultContents; + static MockOptions DefaultOptions = new(); + static MockOptions DefaultPeekOptions = new(); + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Information() + .CreateLogger(); + + var factMockObj = new Mock(); + factMockObj.Setup(x => x.Create()).Returns(SetupFakeProcess(DefaultContents).Object); + DefaultFactory = factMockObj.Object; + + DefaultPeekOptions.peek = false; + } + + private struct MockOptions + { + public bool partialFileInfo = true; + public bool partialFakeProcess = true; + public bool partialDPFileInfo = true; + public bool partialZipArchiveEntry = true; + public bool partialFileSystem = true; + public string[] paths = DefaultContents; + public bool peek = true; + + public MockOptions() { } + } + + private readonly record struct MockOutputs (DP7zExtractor extractor, Mock fakeProcess, Mock fakeDPFileInfo, Mock fakeFileInfo, Mock fakeFileSystem, Mock factory); + + private static DPArchive NewMockedArchive(MockOptions options, out MockOutputs mockOutputs) + { + var fakeProcess = SetupFakeProcess(options.paths); + var fakeFileInfo = new Mock("Z:/test.7z") { CallBase = options.partialFileInfo }; + var fakeFileSystem = new Mock(DPFileScopeSettings.All) { CallBase = options.partialFileSystem }; + var fakeDPFileInfo = new Mock(fakeFileInfo.Object, fakeFileSystem.Object, null) { CallBase = options.partialDPFileInfo }; + var factory = new Mock(); + factory.Setup(x => x.Create()).Returns(fakeProcess.Object); + var extractor = new DP7zExtractor(Log.Logger.ForContext(), factory.Object); + var arc = new DPArchive(string.Empty, Log.Logger.ForContext(), fakeDPFileInfo.Object, extractor); + mockOutputs = new MockOutputs(extractor, fakeProcess, fakeDPFileInfo, fakeFileInfo, fakeFileSystem, factory); + if (!options.peek) return arc; + extractor.Peek(arc); + fakeProcess.Object.OutputEnumerable = new[] { "Everything is Ok" }; + return arc; + } + + /// + /// Returns a new and sets up the to contain the given paths. + /// + /// The entries to add to the fake archive. + internal static Mock SetupFakeProcess(IEnumerable paths, bool partial = true) + { + var l = new List(); + foreach (var p in paths) FakeProcess.GetLinesForEntity(p, l); + var proc = partial ? new Mock() { CallBase = true } : new Mock(); + proc.Object.OutputEnumerable = l; + return proc; + } + + public static DPExtractionReport RunAndAssertExtractEvents(DPAbstractExtractor extractor, DPExtractSettings settings, bool toTemp = false) + { + bool extracting = false, moving = false; + extractor.Extracting += () => + { + if (extracting) + Assert.Fail("Extracting event was raised more than once"); + extracting = true; + }; + extractor.Moving += () => + { + if (moving && !toTemp) + Assert.Fail("Moving event was raised more than once"); + else if (!moving && toTemp) Assert.Fail("Moving event was called when extracting to temp"); + moving = true; + }; + bool extractFinished = false, moveFinished = false; + extractor.ExtractFinished += () => + { + if (extractFinished) + Assert.Fail("ExtractFinished event was raised more than once"); + extractFinished = true; + }; + extractor.MoveFinished += () => + { + if (moveFinished && !toTemp) + Assert.Fail("MoveFinished event was raised more than once"); + else if (!moveFinished && toTemp) Assert.Fail("MoveFinished event was called when extracting to temp"); + moveFinished = true; + }; + var report = toTemp ? extractor.ExtractToTemp(settings) : extractor.Extract(settings); + Assert.IsTrue(extracting, "Extracting event was not raised"); + Assert.IsTrue(extractFinished, "ExtractFinished event was not raised"); + if (!toTemp) + { + Assert.IsTrue(moving, "Moving event was not raised"); + Assert.IsTrue(moveFinished, "MoveFinished event was not raised"); + } + return report; + } + + public static void SetupTargetPathsForTemp(DPArchive arc, string basePath) + { + foreach (var file in arc.Contents.Values) + file.TargetPath = Path.Combine(basePath, Path.GetFileNameWithoutExtension(arc.FileName), file.Path); + } + + [TestMethod] + public void DP7zExtractorTest() + { + var l = Mock.Of(); + var e = new DP7zExtractor(l, DefaultFactory); + Assert.AreEqual(l, e.Logger); + Assert.AreEqual(DefaultFactory, e.Factory); + } + + [TestMethod] + public void ExtractTest() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + var e = outputs.extractor; + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_PartialExtract() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + var e = outputs.extractor; + + var skippedFile = arc.Contents.Values.ElementAt(1); + var successFiles = arc.Contents.Values.Except(new[] { skippedFile }).ToList(); + var settings = new DPExtractSettings("Z:/temp", successFiles, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = successFiles, ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(successFiles); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_QuitsOnArcNotExists() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + var e = outputs.extractor; + var arcDPFileInfo = outputs.fakeDPFileInfo; + arcDPFileInfo.Setup(x => x.Exists).Returns(false); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + e.Extracting += () => Assert.Fail("Extracting event was raised"); + var report = e.Extract(settings); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_NoContents() + { + var arc = NewMockedArchive(new MockOptions() { paths = Array.Empty() }, out var outputs); + var e = outputs.extractor; + var proc = outputs.fakeProcess; + proc.Object.OutputEnumerable = new[] {null, "Everything is Ok"}; + var settings = new DPExtractSettings("Z:/temp", Array.Empty(), archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_ArcFileInfoOpenFail() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var _, out var arcDPFileInfo, out var _, out var _, out var _); + arcDPFileInfo.Setup(x => x.OpenRead()).Throws(new Exception("Something went wrong")); + + var settings = new DPExtractSettings("Z:/temp", new DPFile[] { new(), new(), new() }, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_FileNotPartOfArchive() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var _, out var _, out var _, out var _, out var _); + arc.Contents.Values.First().AssociatedArchive = new DPArchive(); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var successFiles = arc.Contents.Values.Skip(1).ToList(); + var expectedReport = new DPExtractionReport() + { + ExtractedFiles = successFiles, + ErroredFiles = new() { { arc.Contents.Values.First(), "" } }, + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, successFiles.Select(x => x.Path)); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(successFiles); + } + [TestMethod] + public void ExtractTest_FilesNotWhitelisted() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var _, out var arcDPFileInfo, out var _, out var _, out var _); + e.Peek(arc); + outputs.fakeFileSystem.SetupProperty(x => x.Scope, DPFileScopeSettings.None); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() + { + ExtractedFiles = new(0), + ErroredFiles = arc.Contents.Values.ToDictionary(x => x, x => ""), + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_UnexpectedExtractErrorEverythingFine() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + + var e = outputs.extractor; + var proc = outputs.fakeProcess; + proc.Object.ErrorEnumerable = new[] { "i am a teapot" }; + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtract() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var _, out var _, out var _, out var _, out var _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.Extract(settings); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abcd/"); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtractError() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var _, out var _, out var _, out var _, out var _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + settings.FilesToExtract.Add(null); + e.Extract(settings); + settings.FilesToExtract.Remove(null); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtractTemp() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var _, out var _, out var _, out var _, out var _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.ExtractToTemp(settings); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abcd/"); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + + [TestMethod] + public void ExtractToTempTest() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var _, out var _, out var _, out var _, out var _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + SetupTargetPathsForTemp(arc, settings.TempPath); // This should not matter, but will reuse this for testing purposes for AssertExtractorSetPathsCorrectly + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings, true); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + + } + [TestMethod] + public void ExtractToTempTest_AfterExtract() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var _, out var _, out var _, out var _, out var _); + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + SetupTargetPathsForTemp(arc, settings.TempPath); // ExtractToTemp does not require TargetPaths + arc.ExtractContents(settings); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings, true); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void PeekTest() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var _, out var _, out var _, out var _, out var _); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void PeekTest_EmitsWithErrors() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var fakeProcess, out var _, out var _, out var _, out var _); + fakeProcess.Object.ErrorEnumerable = new[] { "Can not open encrypted archive. Wrong password? You silly goose." }; + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + e.ArchiveErrored += (s, e) => Assert.AreEqual("Can not open encrypted archive. Wrong password? You silly goose.", e.Explaination); + } + [TestMethod] + public void PeekTest_StartProcessFails() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var fakeProcess, out var _, out var _, out var _, out var _); + fakeProcess.Setup(x => x.Start()).Throws(new Exception("Something went wrong")); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + e.ArchiveErrored += (s, e) => Assert.AreEqual("Failed to start 7z process", e.Explaination); + } + + [TestMethod] + public void PeekTest_Encrypted() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var fakeProcess, out var _, out var _, out var _, out var _); + var l = new List(); + FakeProcess.GetLinesForEntity("encrypted_something.jpg", l); + l.Insert(3, "Encrypted = +"); + fakeProcess.Object.OutputEnumerable = fakeProcess.Object.OutputEnumerable.Concat(l); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + e.ArchiveErrored += (s, e) => Assert.AreEqual(DPArchiveErrorArgs.EncryptedFilesExplanation, e.Explaination); + } + [TestMethod] + public void ExtractTest_CancelledBeforeOp() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + var e = outputs.extractor; + + CancellationTokenSource cts = new(); + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc) { CancelToken = cts.Token }; + cts.Cancel(true); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = e.Extract(settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_CancelledDuringExtractOp() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + var e = outputs.extractor; + + CancellationTokenSource cts = new(); + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc) { CancelToken = cts.Token }; + outputs.fakeProcess.Object.OutputDataReceived += _ => cts.Cancel(true); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_CancelledDuringMoveOp() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + var e = outputs.extractor; + + CancellationTokenSource cts = new(); + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc) { CancelToken = cts.Token }; + e.MoveProgress += (_, __) => cts.Cancel(true); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(1) { arc.Contents.First().Value }, ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void PeekTest_CancelledBeforeOp() + { + var arc = NewMockedArchive(DefaultOptions with { peek = false }, out var outputs); + outputs.Deconstruct(out var e, out var _, out var _, out var _, out var _, out var _); + e.CancellationToken = new(true); + // Testing Peek() here: + e.Peek(arc); + + Assert.AreEqual(0, arc.Contents.Count); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void PeekTest_CancelledDuringOp() + { + var arc = NewMockedArchive(DefaultOptions with { peek = false }, out var outputs); + outputs.Deconstruct(out var e, out var _, out var _, out var _, out var _, out var _); + CancellationTokenSource cts = new(); + e.CancellationToken = cts.Token; + outputs.fakeProcess.Object.OutputDataReceived += _ => cts.Cancel(true); + + // Testing Peek() here: + e.Peek(arc); + + Assert.AreEqual(0, arc.Contents.Count); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/Extraction/DPRARExtractorTests.cs b/src/DAZ_Installer.CoreTests/Extraction/DPRARExtractorTests.cs new file mode 100644 index 0000000..1c4bc08 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/DPRARExtractorTests.cs @@ -0,0 +1,461 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using DAZ_Installer.Core.Extraction.Fakes; +using DAZ_Installer.CoreTests.Extraction; +using DAZ_Installer.IO.Fakes; +using DAZ_Installer.IO; +using Moq; +using Serilog; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using DAZ_Installer.External; +using System.Runtime.CompilerServices; +#pragma warning disable 618 +namespace DAZ_Installer.Core.Extraction.Tests +{ + [TestClass] + public class DPRARExtractorTests + { + /// + /// A factory that returns a mocked fake archive with the default contents. + /// + static IRARFactory DefaultFactory { get; set; } = null!; + static string[] DefaultContents => DPArchiveTestHelpers.DefaultContents; + static MockOptions DefaultOptions = new(); + static MockOptions DefaultPeekOptions = new(); + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Information() + .CreateLogger(); + + var mock = new Mock(); + mock.Setup(x => x.Create(It.IsAny())).Returns(SetupFakeRAR(DefaultContents).Object); + DefaultFactory = mock.Object; + DefaultPeekOptions.peek = false; + } + + private struct MockOptions + { + public bool partialFileInfo = true; + public bool partialRAR = true; + public bool partialDPFileInfo = true; + public bool partialZipArchiveEntry = true; + public bool partialFileSystem = true; + public string[] paths = DefaultContents; + public bool peek = true; + + public MockOptions() { } + } + + private static DPArchive SetupArchiveWithPartiallyFakedDependencies(MockOptions options, out DPRARExtractor extractor, out Mock fakeRAR, out Mock fakeDPFileInfo, out Mock fakeFileInfo, out Mock factory, out Mock fakeFileSystem) + { + var fs = new Mock(DPFileScopeSettings.All) { CallBase = options.partialFileSystem }; + fakeFileSystem = fs; + factory = new Mock(); + var aFakeRAR = SetupFakeRAR(options.paths, options.partialRAR); + fakeRAR = aFakeRAR; + fakeFileInfo = new Mock("Z:/test.rar") { CallBase = options.partialFileInfo }; + fakeDPFileInfo = new Mock(fakeFileInfo.Object, fakeFileSystem.Object, null) { CallBase = options.partialDPFileInfo }; + factory.Setup(x => x.Create(It.IsAny())).Returns(() => + { + aFakeRAR.Object.Disposed = false; + return aFakeRAR.Object; + }); + extractor = new DPRARExtractor(Log.Logger.ForContext(), factory.Object); + var arc = new DPArchive(string.Empty, Log.Logger.ForContext(), fakeDPFileInfo.Object, extractor); + if (!options.peek) return arc; + extractor.Peek(arc); + return arc; + } + + /// + /// Returns a new and sets up the to contain the given paths. + /// + /// The entries to add to the fake archive. + internal static Mock SetupFakeRAR(IEnumerable paths, bool partial = true) + { + var l = new List(); + foreach (var p in paths) + l.Add(FakeRAR.CreateFileInfoForEntity(p)); + return partial ? new Mock(l) { CallBase = true } : new Mock(l); + } + + public static void SetupTargetPathsForTemp(DPArchive arc, string basePath) + { + foreach (var file in arc.Contents.Values) + file.TargetPath = Path.Combine(basePath, Path.GetFileNameWithoutExtension(arc.FileName), file.Path); + } + + [TestMethod] + public void DPRARExtractorTest() + { + var l = Mock.Of(); + var e = new DPRARExtractor(l, DefaultFactory); + Assert.AreEqual(l, e.Logger); + Assert.AreEqual(DefaultFactory, e.Factory); + } + + [TestMethod] + public void ExtractTest() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_QuitsOnArcNotExists() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out var arcDPFileInfo, out _, out _, out _); + arcDPFileInfo.SetupGet(x => x.Exists).Returns(false); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_NoContents() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(new MockOptions() { paths = Array.Empty() }, out var e, out var proc, out _, out _, out _, out _); + var settings = new DPExtractSettings("Z:/temp", Array.Empty(), archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_RAROpenFail() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var fakeRAR, out _, out _, out var factory, out _); + fakeRAR.Setup(x => x.Open(It.IsAny())).Throws(new Exception("no no no... we no do that here")); + + var settings = new DPExtractSettings("Z:/temp", new DPFile[] { new(), new(), new() }, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_FileNotPartOfArchive() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + arc.Contents.Values.First().AssociatedArchive = null; + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var successFiles = arc.Contents.Values.Skip(1).ToList(); + var expectedReport = new DPExtractionReport() + { + ExtractedFiles = successFiles, + ErroredFiles = new() { { arc.Contents.Values.First(), "" } }, + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, successFiles.Select(x => x.Path)); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(successFiles); + } + [TestMethod] + public void ExtractTest_PartialExtract() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + var skippedFile = arc.Contents.Values.ElementAt(1); + var successFiles = arc.Contents.Values.Except(new[] { skippedFile }).ToList(); + var settings = new DPExtractSettings("Z:/temp", successFiles, archive: arc); + var expectedReport = new DPExtractionReport() + { + ExtractedFiles = successFiles, + ErroredFiles = new(0), + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, successFiles.Select(x => x.Path)); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(successFiles); + } + + [TestMethod] + public void ExtractTest_FilesNotWhitelisted() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out var fs); + fs.Object.Scope = DPFileScopeSettings.None; + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() + { + ExtractedFiles = new(0), + ErroredFiles = arc.Contents.Values.ToDictionary(x => x, x => ""), + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_UnexpectedExtractErrorEverythingFine() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var proc, out _, out _, out _, out _); + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtract() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.Extract(settings); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abcd/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtractError() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var fakeArc, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + settings.FilesToExtract.Add(null); + e.Extract(settings); + settings.FilesToExtract.Remove(null); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtractTemp() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.ExtractToTemp(settings); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abcd/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + + [TestMethod] + public void ExtractToTempTest() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + SetupTargetPathsForTemp(arc, settings.TempPath); // This should not matter, but will reuse this for testing purposes for AssertExtractorSetPathsCorrectly + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings, true); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + + } + [TestMethod] + public void ExtractToTempTest_AfterExtract() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + SetupTargetPathsForTemp(arc, settings.TempPath); // ExtractToTemp does not require TargetPaths + arc.ExtractContents(settings); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings, true); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void PeekTest() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultPeekOptions, out var e, out var _, out _, out _, out _, out _); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void PeekTest_EmitsWithErrors() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultPeekOptions, out var e, out var fakeRAR, out _, out _, out _, out _); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + e.ArchiveErrored += (s, e) => Assert.AreEqual("Can not open encrypted archive. Wrong password? You silly goose.", e.Explaination); + } + [TestMethod] + public void PeekTest_StartProcessFails() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultPeekOptions, out var e, out var fakeRAR, out _, out _, out _, out _); + fakeRAR.Setup(x => x.ReadHeader()).Throws(new Exception("Something went wrong")); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + e.ArchiveErrored += (s, e) => Assert.AreEqual("Failed to start 7z process", e.Explaination); + } + + [TestMethod] + public void PeekTest_Encrypted() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultPeekOptions, out var e, out var fakeRAR, out _, out _, out _, out _); + var l = new List(); + fakeRAR.Object.FilesEnumerable.MoveNext(); + fakeRAR.Object.FilesEnumerable.Current.encrypted = true; + fakeRAR.Object.FilesEnumerable.Reset(); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + e.ArchiveErrored += (s, e) => Assert.AreEqual(DPArchiveErrorArgs.EncryptedFilesExplanation, e.Explaination); + } + + [TestMethod] + public void ExtractTest_CancelledBeforeOp() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + CancellationTokenSource cts = new(); + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc) { CancelToken = cts.Token }; + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + cts.Cancel(true); + + // Testing Extract() here: + var report = e.Extract(settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_CancelledDuringOp() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + CancellationTokenSource cts = new(); + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc) { CancelToken = cts.Token }; + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(1) { arc.Contents.First().Value }, ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.ExtractProgress += (s, e) => cts.Cancel(true); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + + } + + [TestMethod] + public void PeekTest_CancelledBeforeOp() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultPeekOptions, out var e, out var _, out _, out _, out _, out _); + e.CancellationToken = new(true); + // Testing Peek() here: + e.Peek(arc); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + Assert.AreEqual(0, arc.Contents.Count); + } + + [TestMethod] + public void PeekTest_CancelledDuringOp() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultPeekOptions, out var e, out var r, out _, out _, out _, out _); + r.Setup(x => x.ReadHeader()).Callback(() => e.CancellationToken = new(true)); + + // Testing Peek() here: + e.Peek(arc); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + Assert.AreEqual(0, arc.Contents.Count); + } + + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/Extraction/DPZipExtractorTests.cs b/src/DAZ_Installer.CoreTests/Extraction/DPZipExtractorTests.cs new file mode 100644 index 0000000..9b2f70e --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/DPZipExtractorTests.cs @@ -0,0 +1,513 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using Moq; +using Serilog; +using DAZ_Installer.IO.Fakes; +using DAZ_Installer.IO; +using DAZ_Installer.CoreTests.Extraction; +using DAZ_Installer.Core.Extraction.Fakes; + +#pragma warning disable CS0618 // Obsolete is for production code, not testing code. +namespace DAZ_Installer.Core.Extraction.Tests +{ + [TestClass] + public class DPZipExtractorTests + { + /// + /// A factory that returns a mocked fake archive with the default contents. + /// + static IZipArchiveFactory DefaultFactory { get; set; } = null!; + static string[] DefaultContents => DPArchiveTestHelpers.DefaultContents; + static MockOptions DefaultOptions = new(); + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .CreateLogger(); + var mock = new Mock(); + mock.Setup(m => m.Create(It.IsAny())).Returns(() => SetupFakeArchiveAndEntries(DefaultContents).Object); + DefaultFactory = mock.Object; + } + + private struct MockOptions + { + public bool partialFileInfo = true; + public bool partialDPFileInfo = true; + public bool partialZipArchiveEntry = true; + public bool partialFileSystem = true; + public string[] paths = DefaultContents; + public bool peek = true; + + public MockOptions() { } + } + + private static DPArchive NewMockedArchive(MockOptions options, out DPZipExtractor extractor, out Mock fakeArc, out Mock fakeDPFileInfo, out Mock fakeFileInfo, out Mock factory, out Mock fakeFileSystem) + { + var fs = new Mock(DPFileScopeSettings.All) { CallBase = options.partialFileSystem }; + fakeFileSystem = fs; + fakeArc = SetupFakeArchiveAndEntries(options.paths, options.partialZipArchiveEntry); + fakeFileInfo = new Mock("Z:/test.zip") { CallBase = options.partialFileInfo }; + fakeDPFileInfo = new Mock(fakeFileInfo.Object, fs.Object, null) { CallBase = options.partialFileInfo }; + factory = new Mock(); + factory.Setup(m => m.Create(It.IsAny())).Returns(fakeArc.Object); + extractor = new DPZipExtractor(Log.Logger.ForContext(), factory.Object); + var arc = new DPArchive(string.Empty, Log.Logger.ForContext(), fakeDPFileInfo.Object, extractor); + if (options.peek) extractor.Peek(arc); + return arc; + } + + /// + /// Returns a new and sets up mocked s for the specified paths. + /// + /// The entries to add to the fake archive. + /// Whether to partially mock the entries, default is true. + /// A new with partially mocked s. + internal static Mock SetupFakeArchiveAndEntries(IEnumerable paths, bool partial = true) + { + var arc = new Mock() { CallBase = true }; + foreach (var path in paths) + { + var entryMock = new Mock(arc.Object, Stream.Null) { CallBase = partial }; + if (!partial) entryMock.SetupGet(x => x.FullName).Returns(path); + else entryMock.Object.FullName = path; + if (!partial) entryMock.SetupGet(x => x.Name).Returns(Path.GetFileName(path)); + arc.Object.PathToEntries.Add(entryMock.Object.FullName, entryMock.Object); + } + return arc; + } + + [TestMethod] + public void DPZipExtractorTest() + { + var l = Mock.Of(); + var e = new DPZipExtractor(l, DefaultFactory); + Assert.AreEqual(l, e.Logger); + Assert.AreEqual(DefaultFactory, e.Factory); + } + + [TestMethod] + public void ExtractTest() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_PartialExtract() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var skippedFile = arc.Contents.Values.ElementAt(1); + var successFiles = arc.Contents.Values.Except(new[] { skippedFile }).ToList(); + var settings = new DPExtractSettings("Z:/temp", successFiles, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(successFiles), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(successFiles); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_QuitsOnArcNotExists() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out var arcDPFileInfo, out _, out _, out _); + arcDPFileInfo.SetupGet(x => x.Exists).Returns(false); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.Extracting += () => Assert.Fail("Extracting event was raised"); + + // Testing Extract() here: + var report = e.Extract(settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_NoContents() + { + var arc = NewMockedArchive(new MockOptions() { paths = Array.Empty() }, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", Array.Empty(), archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_ArcFileInfoOpenFail() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out var arcDPFileInfo, out _, out _, out _); + arcDPFileInfo.Setup(x => x.OpenRead()).Throws(new IOException()); + + var settings = new DPExtractSettings("Z:/temp", new DPFile[] {new(), new(), new()}, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_FileNotPartOfArchive() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + arc.Contents.Values.First().AssociatedArchive = new DPArchive(); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { + ExtractedFiles = arc.Contents.Values.Skip(1).ToList(), + ErroredFiles = new() { { arc.Contents.Values.First(), "" } }, + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_FilesNotWhitelisted() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out var fs); + e.Peek(arc); + fs.Object.Scope = DPFileScopeSettings.None; + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() + { + ExtractedFiles = new(0), + ErroredFiles = arc.Contents.Values.ToDictionary(x => x, x => ""), + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_RetrySuccessUnauthorizedFileException() + { + var arc = NewMockedArchive(new() { partialZipArchiveEntry = false }, out var e, out var zipArc, out _, out _, out _, out var _); + + var firstEntityName = zipArc.Object.PathToEntries.Values.Where(x => !string.IsNullOrEmpty(x.Name)).First().FullName; + var subEntity = new Mock(zipArc.Object, Stream.Null) { CallBase = true }; + var calledOnce = false; + subEntity.Object.FullName = firstEntityName; + subEntity.Setup(x => x.ExtractToFile(It.IsAny(), It.IsAny())).Callback(() => + { + if (calledOnce) return; + calledOnce = true; + Log.Logger.Information("Throwing UnauthorizedAccessException"); + throw new UnauthorizedAccessException(); + }); + zipArc.Object.PathToEntries[firstEntityName] = subEntity.Object; + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() + { + ExtractedFiles = arc.Contents.Values.ToList(), + ErroredFiles = new(0), + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_RetryFailUnauthorizedFileException() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var zipArc, out _, out _, out _, out _); + + var firstEntityName = zipArc.Object.PathToEntries.Values.Where(x => !string.IsNullOrEmpty(x.Name)).First().FullName; + var subEntity = new Mock(); + subEntity.Setup(x => x.ExtractToFile(It.IsAny(), It.IsAny())).Throws(new UnauthorizedAccessException()); + zipArc.Object.PathToEntries[firstEntityName] = subEntity.Object; + + var file = arc.Contents[PathHelper.NormalizePath(firstEntityName)]; + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() + { + ExtractedFiles = arc.Contents.Values.Where(x => x != file).ToList(), + ErroredFiles = new() { { file, ""} }, + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, expectedReport.ExtractedFiles.Select(x => x.Path)); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(expectedReport.ExtractedFiles); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_UnexpectedExtractError() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var zipArc, out _, out _, out _, out _); + + var firstEntityName = zipArc.Object.PathToEntries.Values.Where(x => !string.IsNullOrEmpty(x.Name)).First().FullName; + var subEntity = new Mock(); + zipArc.Object.PathToEntries[firstEntityName] = null; + + var file = arc.Contents[PathHelper.NormalizePath(firstEntityName)]; + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() + { + ExtractedFiles = arc.Contents.Values.Where(x => x != file).ToList(), + ErroredFiles = new() { { file, "" } }, + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, expectedReport.ExtractedFiles.Select(x => x.Path)); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(expectedReport.ExtractedFiles); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtract() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.Extract(settings); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abcd/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtractError() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var fakeArc, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + settings.FilesToExtract.Add(null); + e.Extract(settings); + settings.FilesToExtract.Remove(null); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtractTemp() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.ExtractToTemp(settings); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abcd/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + + [TestMethod] + public void ExtractToTempTest() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPathsForTemp(arc, settings.TempPath); // This should not matter, but will reuse this for testing purposes for AssertExtractorSetPathsCorrectly + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings, true); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + + } + [TestMethod] + public void ExtractToTempTest_AfterExtract() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPathsForTemp(arc, settings.TempPath); // ExtractToTemp does not require TargetPaths, it will do + arc.ExtractContents(settings); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings, true); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void PeekTest() + { + var e = new DPZipExtractor(Log.Logger, DefaultFactory); + var dpFileInfo = new FakeDPFileInfo(new FakeFileInfo("Z:/test.zip", null), new(), null); + var arc = new DPArchive(string.Empty, Log.Logger, dpFileInfo, e); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void PeekTest_EmitsWithErrors() + { + var erroringFactory = new Mock(); + erroringFactory.Setup(erroringFactory => erroringFactory.Create(It.IsAny())).Throws(new Exception("Test exception")); + var e = new DPZipExtractor(Log.Logger, erroringFactory.Object); + var dpFileInfo = new FakeDPFileInfo(new FakeFileInfo("Z:/test.zip", null), new(), null); + var arc = new DPArchive(string.Empty, Log.Logger, dpFileInfo, e); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + } + [TestMethod] + public void PeekTest_SeekingErrorStopsSeeking() + { + var erroringFactory = new Mock(); + erroringFactory.Setup(erroringFactory => erroringFactory.Create(It.IsAny())).Throws(new Exception("Test exception")); + var e = new DPZipExtractor(Log.Logger, erroringFactory.Object); + var dpFileInfo = new FakeDPFileInfo(new FakeFileInfo("Z:/test.zip", null), new(), null); + var arc = new DPArchive(string.Empty, Log.Logger, dpFileInfo, e); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + } + + [TestMethod] + public void ExtractTest_CancelledBeforeOp() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.CancellationToken = new(true); + + // Testing Extract() here: + var report = e.Extract(settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_CancelledDuringOp() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(1) { arc.Contents.First().Value }, ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.ExtractProgress += (_, __) => e.CancellationToken = new(true); + + // Testing Extract() here: + var report = e.Extract(settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void PeekTest_CancelledBeforeOp() + { + var arc = NewMockedArchive(DefaultOptions with { peek = false }, out var e, out var a, out _, out _, out _, out _); + e.CancellationToken = new(true); + + // Testing Peek() here: + // Testing Peek() here: + e.Peek(arc); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + Assert.AreEqual(0, arc.Contents.Count); + } + + [TestMethod] + public void PeekTest_CancelledDuringOp() + { + var arc = NewMockedArchive(DefaultOptions with { peek = false }, out var e, out var a, out _, out _, out _, out _); + a.Setup(x => x.Entries).Callback(() => e.CancellationToken = new(true)); + + // Testing Peek() here: + e.Peek(arc); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + Assert.AreEqual(0, arc.Contents.Count); + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeProcess.cs b/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeProcess.cs new file mode 100644 index 0000000..8e1df89 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeProcess.cs @@ -0,0 +1,94 @@ +using System.Diagnostics; + +namespace DAZ_Installer.Core.Extraction.Fakes +{ + /// + /// A fake process that implements and all implemented properties and methods are virtual. + /// + internal class FakeProcess : IProcess + { + public virtual StreamWriter StandardInput => StreamWriter.Null; + public virtual ProcessStartInfo StartInfo { get; set; } = new(); + public virtual bool HasExited { get; set; } + public virtual bool EnableRaisingEvents { get; set; } = false; + + public virtual event Action? OutputDataReceived; + public virtual event Action? ErrorDataReceived; + public virtual event Action? Exited; + + public virtual IEnumerable OutputEnumerable { get; set; } = Enumerable.Empty(); + public virtual IEnumerable ErrorEnumerable { get; set; } = Enumerable.Empty(); + public virtual IEnumerator? OutputEnumerator { get; set; } = null; + public virtual IEnumerator? ErrorEnumerator { get; set; } = null; + /// + /// Begins raising the event. + /// It will emit error data to the event via . + /// Additionally, if any values are provided, it will stop until the next call to . + /// This method is virtual. + /// + public virtual void BeginErrorReadLine() + { + if (!EnableRaisingEvents) throw new Exception("EnableRaisingEvents must be true to use this method."); + ErrorEnumerator ??= ErrorEnumerable.GetEnumerator(); + while (ErrorEnumerator.MoveNext()) + { + var line = ErrorEnumerator.Current; + ErrorDataReceived?.Invoke(line); + if (line is null) return; + } + ErrorEnumerator = null; + ErrorDataReceived?.Invoke(null); + HasExited = true; + } + /// + /// Begins raising the event. + /// It will emit error data to the event via . + /// Additionally, if any values are provided, it will stop until the next call to . + /// + /// This method is virtual. + /// + public virtual void BeginOutputReadLine() + { + if (!EnableRaisingEvents) throw new Exception("EnableRaisingEvents must be true to use this method."); + OutputEnumerator ??= OutputEnumerable.GetEnumerator(); + while (OutputEnumerator.MoveNext()) + { + var line = OutputEnumerator.Current; + OutputDataReceived?.Invoke(line); + if (line is null) return; + } + OutputEnumerator = null; + OutputDataReceived?.Invoke(null); + } + /// + /// Does nothing. + /// + public virtual void Dispose() { } + /// + /// Sets to true. + /// + /// does nothing + public virtual void Kill(bool entireProcessTree) => HasExited = true; + /// + /// Does nothing. + /// + public virtual void Start() { } + public virtual bool WaitForExit(int milliseconds) => SpinWait.SpinUntil(() => HasExited, milliseconds); + /// + /// Returns an array of strings that represent 7z output. At [0] = path, [1] = size, [2] = attributes, [3] = separator. + /// + /// The path to get 7z info from. + /// The lines of output for the entity. + public static void GetLinesForEntity(string entity, in List listToAddStringsTo) + { + var fname = Path.GetFileName(entity); + listToAddStringsTo.AddRange(new[] + { + "----------", + "Path = " + entity, + "Size = " + 1, + "Attributes = " + (string.IsNullOrEmpty(fname) ? "D" : ""), + }); + } + } +} diff --git a/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeRAR.cs b/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeRAR.cs new file mode 100644 index 0000000..36b62fc --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeRAR.cs @@ -0,0 +1,143 @@ +using DAZ_Installer.External; + +namespace DAZ_Installer.Core.Extraction.Fakes +{ + internal class FakeRAR : IRAR + { + public IEnumerator FilesEnumerable; + public virtual bool Disposed { get; set; } = false; + public virtual bool Closed { get; set; } = true; + /// + /// ActionCalled is a variable used to determine if after a call to , + /// that either , , or was called. + /// + public virtual bool ActionCalled { get; set; } = true; + public virtual RAR.OpenMode Mode { get; set; } = RAR.OpenMode.List; + + public virtual event RAR.MissingVolumeHandler? MissingVolume; + public virtual event RAR.NewFileHandler? NewFile; + public virtual event RAR.PasswordRequiredHandler? PasswordRequired; + public virtual event RAR.ExtractionProgressHandler? ExtractionProgress; + + internal FakeRAR(IEnumerable files) => FilesEnumerable = files.GetEnumerator(); + + public virtual RARFileInfo CurrentFile => FilesEnumerable.Current; + + /// + /// Returns the value at . + /// + public RAR.RAROpenArchiveDataEx ArchiveData => ArchiveDataToReturn; + public RAR.RAROpenArchiveDataEx ArchiveDataToReturn = new() + { + ArcName = "test.rar", + ArcNameW = "test.rar", + OpenMode = (uint)RAR.OpenMode.List, + Flags = (uint)(RAR.ArchiveFlags.FirstVolume | RAR.ArchiveFlags.Volume), + }; + + public virtual string DestinationPath { get; set; } = string.Empty; + + /// + /// Sets the flag to true. + /// + public virtual void Close() => Closed = true; + /// + /// Sets the flag to true. + /// + public virtual void Dispose() => Disposed = true; + /// + /// Changes destination path and throws if or is true. + /// + /// The destination to extract to. + /// + /// + public virtual void Extract(string destinationName) + { + _ = throwIfDisposed() && throwIfClosed(); + if (Mode != RAR.OpenMode.Extract) throw new InvalidOperationException("Archive is not open for extraction."); + if (CurrentFile is null) throw new InvalidOperationException("No file is selected."); + if (CurrentFile.encrypted) throw new IOException("File could not be opened."); // do not change this err message or type. + DestinationPath = Path.GetDirectoryName(destinationName) ?? string.Empty; + ActionCalled = true; + } + /// + /// Resets the enumerator and sets the flag to false. Throws if is true or is false + /// + /// The mode to use + /// + /// + public virtual void Open(RAR.OpenMode mode) + { + throwIfDisposed(); + if (!Closed) throw new InvalidOperationException("Archive is already open."); + Closed = false; + ActionCalled = true; + Mode = mode; + FilesEnumerable.Reset(); + } + /// + /// Moves the enumerator to the next element in the archive. + /// + /// Whether there are any more elements in the archive. + /// if it is. Otherwise, . + /// + /// + /// + public virtual bool ReadHeader() + { + var a = throwIfDisposed() || throwIfClosed() || throwIfActionNotCalled() || FilesEnumerable.MoveNext(); + if (!a) return a; + if (Mode == RAR.OpenMode.List) + NewFile?.Invoke(this, new NewFileEventArgs(FilesEnumerable.Current)); + ActionCalled = false; + return true; + + } + /// + /// Checks if disposed or closed. If not, throws an exception. + /// + /// + /// + public virtual void Skip() + { + _ = throwIfDisposed() || throwIfClosed(); + ActionCalled = true; + } + /// + /// Throws an exception if is null or or is true. + /// + /// + /// + public virtual void Test() + { + _ = throwIfDisposed() && throwIfClosed(); + ArgumentNullException.ThrowIfNull(FilesEnumerable.Current); + ActionCalled = true; + } + + /// + /// Throws an exception if is true. Always returns false. + /// + /// + private bool throwIfDisposed() => Disposed ? throw new ObjectDisposedException(nameof(FakeRAR)) : false; + /// + /// Throws an exception if is true. Always returns false. + /// + /// + private bool throwIfClosed() => Closed ? throw new InvalidOperationException("Archive is closed.") : false; + + /// + /// Throws an exception if is false and the current mode is set to . Always returns false. + /// + /// + private bool throwIfActionNotCalled() => ActionCalled || Mode != RAR.OpenMode.Extract ? false : + throw new InvalidOperationException("Archive is corrupt."); + + internal static RARFileInfo CreateFileInfoForEntity(string path) => new() + { + UnpackedSize = 1, + FileName = path, + IsDirectory = string.IsNullOrEmpty(Path.GetFileName(path)) + }; + } +} diff --git a/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeZipArchive.cs b/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeZipArchive.cs new file mode 100644 index 0000000..2dfaf97 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeZipArchive.cs @@ -0,0 +1,45 @@ +using System.IO.Compression; + +namespace DAZ_Installer.Core.Extraction.Fakes +{ + internal class FakeZipArchive : IZipArchive + { + public Dictionary PathToEntries = new(); + public FakeZipArchive() { } + public FakeZipArchive(IEnumerable entries) + { + foreach (IZipArchiveEntry entry in entries) + { + PathToEntries.Add(entry.FullName, entry); + } + } + public FakeZipArchive(Dictionary dict) => PathToEntries = dict; + public FakeZipArchive(IEnumerable paths) + { + foreach (var path in paths) + { + var entry = new FakeZipArchiveEntry(this, null) + { + FullName = path, + }; + PathToEntries.Add(path, entry); + } + } + + public virtual IReadOnlyCollection Entries => PathToEntries.Values; + + public virtual ZipArchiveMode Mode { get; set; } + + public virtual IZipArchiveEntry CreateEntry(string entryName) => new FakeZipArchiveEntry(this, null); + /// + /// The name of the archive + /// + /// The entry to find. + /// + public virtual IZipArchiveEntry? GetEntry(string entryName) => PathToEntries.TryGetValue(entryName, out IZipArchiveEntry? entry) ? entry : null; + /// + /// Does nothing. + /// + public virtual void Dispose() { } + } +} diff --git a/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeZipArchiveEntry.cs b/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeZipArchiveEntry.cs new file mode 100644 index 0000000..9833b7f --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeZipArchiveEntry.cs @@ -0,0 +1,27 @@ +namespace DAZ_Installer.Core.Extraction.Fakes +{ + internal class FakeZipArchiveEntry : IZipArchiveEntry + { + Stream stream = Stream.Null; + public virtual IZipArchive Archive { get; set; } + + public virtual string Name => Path.GetFileName(FullName); + + public virtual string FullName { get; set; } = string.Empty; + + public virtual long Length { get; set; } = 0; + + public virtual long CompressedLength { get; set; } = 0; + + public virtual DateTimeOffset LastWriteTime { get; set; } = new DateTimeOffset(); + public FakeZipArchiveEntry(IZipArchive Archive, Stream? stream) + { + this.Archive = Archive; + this.stream = stream ?? Stream.Null; + } + public virtual void Delete() { } + public virtual void ExtractToFile(string destinationFileName) { } + public virtual void ExtractToFile(string destinationFileName, bool overwrite) { } + public virtual Stream Open() => stream; + } +} diff --git a/src/DAZ_Installer.CoreTests/Extraction/Helpers/DPArchiveTestHelpers.cs b/src/DAZ_Installer.CoreTests/Extraction/Helpers/DPArchiveTestHelpers.cs new file mode 100644 index 0000000..3f61b95 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/Helpers/DPArchiveTestHelpers.cs @@ -0,0 +1,203 @@ +using DAZ_Installer.Core; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using DAZ_Installer.Core.Extraction.Fakes; +using Moq; +using DAZ_Installer.IO; +using DAZ_Installer.IO.Fakes; +using DAZ_Installer.Core.Extraction; +using Serilog; +using System.Text; +using System.Xml; + +namespace DAZ_Installer.CoreTests.Extraction +{ + internal static class DPArchiveTestHelpers + { + /// + /// The default folders and files to add to an archive. + /// + internal static readonly string[] DefaultContents = new[] { "Contents/", "Contents/a.txt", "b.txt", "Contents/A/c.png" }; + + /// + /// Asserts whether the contents of the archive are as expected. + /// + /// The archive to check. + internal static void AssertDefaultContents(DPArchive arc) + { + Assert.AreEqual(3, arc.Contents.Count, "Archive contents count does not match"); + Assert.AreEqual(1, arc.RootFolders.Count, "Archive root folders count does not match"); + Assert.AreEqual(2, arc.Folders.Count, "Archive folders count does not match"); + Assert.AreEqual(1, arc.RootContents.Count, "Archive root contents count does not match"); + } + + /// + /// Asserts whether the paths of the entities in the archive are as expected (or whether it was added in successfully). + /// + /// The archive to check. + /// The entities to check. + public static void AssertExtractorSetPathsCorrectly(DPArchive arc, IEnumerable entities) + { + foreach (var entity in entities) + { + if (!arc.Contents.ContainsKey(PathHelper.NormalizePath(entity)) && !arc.Folders.ContainsKey(PathHelper.NormalizePath(entity))) + Assert.Fail("Extractor did not correctly set entity paths"); + } + } + + /// + /// Sets up peeking events for the extractor and asserts whether they were raised correctly by peeking into the specified archive. + /// + /// The extractor to use. + /// The archive for to peek into. + public static void RunAndAssertPeekEvents(DPAbstractExtractor extractor, DPArchive arc) + { + bool peeked = false; + extractor.Peeking += () => + { + if (peeked) + Assert.Fail("Peeking event was raised more than once"); + peeked = true; + }; + bool peekFinished = false; + extractor.PeekFinished += () => + { + if (peekFinished) + Assert.Fail("PeekFinished event was raised more than once"); + peekFinished = true; + }; + extractor.Peek(arc); + Assert.IsTrue(peeked, "Peeking event was not raised"); + Assert.IsTrue(peekFinished, "PeekFinished event was not raised"); + } + + /// + /// Sets up peeking events for the extractor and asserts whether they were raised correctly by peeking into the specified archive. + /// + /// The extractor to use. + /// The archive for to peek into. + public static DPExtractionReport RunAndAssertExtractEvents(DPAbstractExtractor extractor, DPExtractSettings settings, bool toTemp = false) + { + bool extracting = false; + extractor.Extracting += () => + { + if (extracting) + Assert.Fail("Extracting event was raised more than once"); + extracting = true; + }; + bool extractFinished = false; + extractor.ExtractFinished += () => + { + if (extractFinished) + Assert.Fail("ExtractFinished event was raised more than once"); + extractFinished = true; + }; + var report = toTemp ? extractor.ExtractToTemp(settings) : extractor.Extract(settings); + Assert.IsTrue(extracting, "Extracting event was not raised"); + Assert.IsTrue(extractFinished, "ExtractFinished event was not raised"); + return report; + } + + public static void AssertExtractFileInfosCorrectlySet(IEnumerable expectedFilesExtracted) + { + foreach (var file in expectedFilesExtracted) + { + Assert.IsNotNull(file.FileInfo, $"{file.FileName}'s FileInfo is null, want not null"); + Assert.AreEqual(PathHelper.NormalizePath(file.TargetPath), + PathHelper.NormalizePath(file.FileInfo!.Path), + $"{file}'s FileInfo's Path does not match TargetPath"); + } + } + + public static void AssertReport(DPExtractionReport want, DPExtractionReport got) + { + CollectionAssert.AreEqual(want.ExtractedFiles, got.ExtractedFiles, "Reports' extracted files are not equal"); + CollectionAssert.AreEqual(want.ErroredFiles.Keys, got.ErroredFiles.Keys, "Reports' errored file keys are not equal"); + Assert.AreEqual(want.Settings, got.Settings, "Reports' settings are not equal"); + } + + public static void SetupTargetPaths(DPArchive arc, string basePath) + { + foreach (var file in arc.Contents.Values) + file.TargetPath = Path.Combine(basePath, arc.FileName, file.FileName); + } + + public static void SetupTargetPathsForTemp(DPArchive arc, string basePath) + { + foreach (var file in arc.Contents.Values) + file.TargetPath = Path.Combine(basePath, file.FileName); + } + + public static Stream CreateMetadataStream(DPFile file, + string type = "wearable", + string author = "TheRealSolly", + string email = "solomon1blount@gmail.com", + string website = "www.thesolomonchronicles.com") + { + var stream = new MemoryStream(); + var json = $@" + {{ + ""file_version"" : ""0.6.0.0"", + ""asset_info"" : {{ + ""id"" : ""/{file.Path}"", + ""type"" : ""{type}"", + ""contributor"" : {{ + ""author"" : ""{author}"", + ""email"" : ""{email}"", + ""website"" : ""{website}"" + }} + }} + }}"; + var sw = new StreamWriter(stream); + sw.Write(json); + sw.Flush(); + stream.Position = 0; + return stream; + + } + + public static Stream CreateManifestStream(DPArchive arc, IEnumerable files) + { + var stream = new MemoryStream(); + var doc = new XmlDocument(); + + var header = doc.CreateElement("DAZInstallManifest"); + header.SetAttribute("VERSION", "0.1"); + doc.AppendChild(header); + + foreach (var file in files) + { + var node = doc.CreateElement("File"); + node.SetAttribute("TARGET", "Content"); + node.SetAttribute("ACTION", "Install"); + node.SetAttribute("VALUE", file); + + header.AppendChild(node); + } + doc.Save(stream); + stream.Position = 0; + return stream; + } + + public static Stream CreateSupplementStream(string productName = "Gentlemen's Library") + { + string supplementStr = "" + + " " + + $" " + + " " + + " " + + ""; + + MemoryStream stream = new(Encoding.ASCII.GetBytes(supplementStr)); + stream.Position = 0; + return stream; + } + public static Stream DetermineFileStream(DPFile file, DPArchive arc, IEnumerable? pathsForManifest = null) + { + if (file.FileName == "Manifest.dsx") return CreateManifestStream(arc, pathsForManifest ?? arc.Contents.Values.Select(x => x.Path)); + else if (file.FileName == "Supplement.dsx") return CreateSupplementStream(); + else if (DPFile.DAZFormats.Contains(file.Ext)) return CreateMetadataStream(file); + return Stream.Null; + } + + } +} diff --git a/src/DAZ_Installer.CoreTests/Extraction/Integration/DP7zExtractorTests.cs b/src/DAZ_Installer.CoreTests/Extraction/Integration/DP7zExtractorTests.cs new file mode 100644 index 0000000..640255d --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/Integration/DP7zExtractorTests.cs @@ -0,0 +1,433 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using DAZ_Installer.Core.Extraction.Fakes; +using DAZ_Installer.CoreTests.Extraction; +using DAZ_Installer.IO; +using DAZ_Installer.IO.Fakes; +using Moq; +using Serilog; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using DAZ_Installer.Core.Integration; +using System.Diagnostics; + +#pragma warning disable 618 +namespace DAZ_Installer.Core.Extraction.Integration.Tests +{ + [TestClass] + public class DP7zExtractorTests + { + public static readonly string TempPath = Path.Combine(Path.GetTempPath(), "DAZ_Installer.CoreTests", "Extraction", "Integration", "Test Subjects"); + public static readonly string RegularArchivePath = Path.Combine(TempPath, "regular.7z"); + public static readonly string SolidArchivePath = Path.Combine(TempPath, "solid.7z"); + public static readonly string EncryptedArchivePath = Path.Combine(TempPath, "encrypted.7z"); + public static readonly string EncryptedHeadersArchivePath = Path.Combine(TempPath, "encrypted.7z"); + public static readonly string MultiVolumeArchivePathInit = Path.Combine(TempPath, "multivolume.7z"); + public static readonly string MultiVolumeArchivePath = Path.Combine(TempPath, "multivolume.7z.001"); + public static readonly string MultiVolumeArchivePath2 = Path.Combine(TempPath, "multivolume.7z.002"); + public static readonly string ArchiveContentsPath = Path.Combine(TempPath, "Archive Contents"); + public static List ArchiveContents = new(5); + // f DRY all my homies copypasta + public static readonly string ExtractPath = Path.Combine(Path.GetTempPath(), "DAZ_InstallerTests", "Extract"); + public static readonly string TestSubjectsPath = Path.Combine(Environment.CurrentDirectory, "Integration", "Test Subjects"); + public static readonly DPFileScopeSettings DefaultScope = new(Enumerable.Empty(), new[] { ExtractPath }, false); + public static readonly DPFileSystem FileSystem = new DPFileSystem(DefaultScope); + public static readonly ProcessStartInfo StartInfoTemplate = new() { FileName = "7za.exe", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; + public static readonly List DefaultOpts = new() + { + "-t7z", // Set archive type to 7z + "-mx0", // Set compression method to simply copy. + "-ms=off" // Set solid mode to off. + }; + public static IEnumerable ArchiveEnumerable => new[] + { + new[] { RegularArchivePath }, + new[] { SolidArchivePath }, + new[] { MultiVolumeArchivePath }, + }; + public static IEnumerable EncryptedArchiveEnumerable => new[] + { + new[] { EncryptedArchivePath }, + new[] { EncryptedHeadersArchivePath }, + }; + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Verbose() + .CreateLogger(); + + ArchiveContents = DPIntegrationArchiveHelpers.CreateArchiveContents(ArchiveContentsPath); + Create7zArchiveOnDisk(RegularArchivePath, ArchiveContentsPath, DefaultOpts); + Create7zArchiveOnDisk(SolidArchivePath, ArchiveContentsPath, DefaultOpts.SkipLast(1)); + Create7zArchiveOnDisk(MultiVolumeArchivePathInit, ArchiveContentsPath, DefaultOpts.Append("-v6m")); + if (!File.Exists(MultiVolumeArchivePath2)) throw new Exception("Failed to create multivolume archive"); + Create7zArchiveOnDisk(EncryptedArchivePath, ArchiveContentsPath, DefaultOpts.Append("-pPASSWORD")); + Create7zArchiveOnDisk(EncryptedHeadersArchivePath, ArchiveContentsPath, DefaultOpts.Append("-pPASSWORD") + .Append("-mhe")); + Directory.CreateDirectory(ExtractPath); + } + [ClassCleanup] + public static void ClassCleanup() + { + Directory.Delete(TempPath, true); + Directory.Delete(ExtractPath, true); + } + + [TestCleanup] + public void TestCleanup() + { + Directory.Delete(ExtractPath, true); + } + + [TestInitialize] + public void TestInitialize() + { + Directory.CreateDirectory(ExtractPath); + } + + public static void SetupTargetPathsForTemp(DPArchive arc, string basePath) + { + foreach (var file in arc.Contents.Values) + file.TargetPath = Path.Combine(basePath, Path.GetFileNameWithoutExtension(arc.FileName), file.Path); + } + + public static DPExtractionReport RunAndAssertExtractEvents(DPAbstractExtractor extractor, DPExtractSettings settings, bool toTemp = false) + { + bool extracting = false, moving = false; + extractor.Extracting += () => + { + if (extracting) + Assert.Fail("Extracting event was raised more than once"); + extracting = true; + }; + extractor.Moving += () => + { + if (moving && !toTemp) + Assert.Fail("Moving event was raised more than once"); + else if (!moving && toTemp) Assert.Fail("Moving event was called when extracting to temp"); + moving = true; + }; + bool extractFinished = false, moveFinished = false; + extractor.ExtractFinished += () => + { + if (extractFinished) + Assert.Fail("ExtractFinished event was raised more than once"); + extractFinished = true; + }; + extractor.MoveFinished += () => + { + if (moveFinished && !toTemp) + Assert.Fail("MoveFinished event was raised more than once"); + else if (!moveFinished && toTemp) Assert.Fail("MoveFinished event was called when extracting to temp"); + moveFinished = true; + }; + var report = toTemp ? extractor.ExtractToTemp(settings) : extractor.Extract(settings); + Assert.IsTrue(extracting, "Extracting event was not raised"); + Assert.IsTrue(extractFinished, "ExtractFinished event was not raised"); + if (!toTemp) + { + Assert.IsTrue(moving, "Moving event was not raised"); + Assert.IsTrue(moveFinished, "MoveFinished event was not raised"); + } + return report; + } + + public static void Create7zArchiveOnDisk(string savePath, string contentsPath, IEnumerable appendedArgs) + { + if (string.IsNullOrWhiteSpace(contentsPath)) throw new ArgumentException("No content path was provided", nameof(contentsPath)); + if (!contentsPath.EndsWith('*')) contentsPath = Path.Join(contentsPath, "*"); + var opts = string.Join(' ', appendedArgs); + var args = $"a {opts} \"{savePath}\" \"{contentsPath}\""; + var proc = new Process() { StartInfo = new() { + FileName = "7za.exe", + Arguments = args, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + proc.Start(); + proc.WaitForExit(); + + Log.Information("Create7zArchiveOnDisk() args: {args}", args); + Log.Information("7z output: {output}", proc.StandardOutput.ReadToEnd()); + if (!proc.StandardError.EndOfStream) Log.Error(proc.StandardError.ReadToEnd()); + + if (opts.Contains("-v")) return; + + // Check if the file exists, if it doesn't throw an error. + if (!File.Exists(savePath)) + throw new Exception($"Failed to create 7z archive on disk at {savePath}"); + } + + [TestMethod] + public void DP7zExtractorTest() + { + var l = Mock.Of(); + var f = new ProcessFactory(); + var e = new DP7zExtractor(l, f); + Assert.AreEqual(l, e.Logger); + Assert.AreEqual(f, e.Factory); + } + + [TestMethod] + [DynamicData(nameof(ArchiveEnumerable), DynamicDataSourceType.Property)] + public void ExtractTest(string path) + { + var fi = FileSystem.CreateFileInfo(path); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + [Timeout(6000)] + [DynamicData(nameof(EncryptedArchiveEnumerable), DynamicDataSourceType.Property)] + public void ExtractTest_Encrypted(string path) + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(5)); + var fi = FileSystem.CreateFileInfo(path); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values, archive: arc); + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + + // Testing Extract() here + bool encryptedFilesShowed = false; + e.ArchiveErrored += (a, b) => + { + Assert.IsFalse(encryptedFilesShowed, "Encrypted files event called more than once."); + if (b.Explaination == DPArchiveErrorArgs.EncryptedFilesExplanation) encryptedFilesShowed = true; + }; + var report = e.Extract(settings); + + Assert.IsTrue(encryptedFilesShowed, "Encrypted files event was not called."); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_FilesNotWhitelisted() + { + var scope = new DPFileScopeSettings(Array.Empty(), new[] { Path.Combine(ExtractPath, "regular") }, true); + var fi = new DPFileSystem(scope).CreateFileInfo(RegularArchivePath); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = arc.Contents.Values.ToDictionary(x => x, x => ""), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_AfterExtract() + { + var fi = FileSystem.CreateFileInfo(RegularArchivePath); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + e.Extract(settings); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_AfterExtractTemp() + { + var fi = FileSystem.CreateFileInfo(RegularArchivePath); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + e.ExtractToTemp(settings); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + + [TestMethod] + [DynamicData(nameof(ArchiveEnumerable), DynamicDataSourceType.Property)] + public void ExtractToTempTest(string path) + { + var fi = FileSystem.CreateFileInfo(path); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + SetupTargetPathsForTemp(arc, ExtractPath); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings, true); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + + } + [TestMethod] + public void ExtractToTempTest_AfterExtract() + { + var fi = FileSystem.CreateFileInfo(RegularArchivePath); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + SetupTargetPathsForTemp(arc, ExtractPath); + e.Extract(settings); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings, true); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + [DynamicData(nameof(ArchiveEnumerable), DynamicDataSourceType.Property)] + public void PeekTest(string path) + { + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()); + var fi = FileSystem.CreateFileInfo(path); + var arc = new DPArchive(fi); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, arc.Contents.Values.Select(x => x.Path)); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + [DynamicData(nameof(EncryptedArchiveEnumerable), DynamicDataSourceType.Property)] + public void PeekTest_Encrypted(string path) + { + var e = new DP7zExtractor(); + var fi = FileSystem.CreateFileInfo(path); + var arc = new DPArchive(fi); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_CancelledBeforeOp() + { + var fi = FileSystem.CreateFileInfo(RegularArchivePath); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + e.CancellationToken = new CancellationToken(true); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_CancelledDuringExtractOp() + { + var processFactory = new Mock(); + var proc = new ProcessWrapper(); + proc.OutputDataReceived += _ => + processFactory.SetupSequence(x => x.Create()).Returns(new ProcessWrapper()) + .Returns(proc); + + var fi = FileSystem.CreateFileInfo(RegularArchivePath); + var cts = new CancellationTokenSource(); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()) { CancellationToken = cts.Token }; + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + proc.OutputDataReceived += _ => cts.Cancel(true); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_CancelledDuringMoveOp() + { + var processFactory = new Mock(); + var proc = new ProcessWrapper(); + proc.OutputDataReceived += _ => + processFactory.SetupSequence(x => x.Create()).Returns(new ProcessWrapper()) + .Returns(proc); + + var fi = FileSystem.CreateFileInfo(RegularArchivePath); + var cts = new CancellationTokenSource(); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()) { CancellationToken = cts.Token }; + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(1) { arc.Contents.First().Value }, ErroredFiles = new(0), Settings = settings }; + + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + e.MoveProgress += (_, __) => cts.Cancel(true); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/Extraction/Integration/DPRARExtractorTests.cs b/src/DAZ_Installer.CoreTests/Extraction/Integration/DPRARExtractorTests.cs new file mode 100644 index 0000000..bb11dc2 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/Integration/DPRARExtractorTests.cs @@ -0,0 +1,222 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using DAZ_Installer.Core.Extraction.Fakes; +using DAZ_Installer.CoreTests.Extraction; +using DAZ_Installer.IO.Fakes; +using DAZ_Installer.IO; +using Moq; +using Serilog; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using DAZ_Installer.External; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Castle.DynamicProxy.Generators; +#pragma warning disable 618 +namespace DAZ_Installer.Core.Extraction.Integration.Tests +{ + [TestClass] + public class DPRARExtractorTests + { + public static readonly string ExtractPath = Path.Combine(Path.GetTempPath(), "DAZ_InstallerTests", "Extract"); + public static readonly string TestSubjectsPath = Path.Combine(Environment.CurrentDirectory, "Integration", "Test Subjects"); + public static readonly DPFileScopeSettings DefaultScope = new(Enumerable.Empty(), new[] { ExtractPath }, false); + public static readonly DPFileSystem FileSystem = new DPFileSystem(DefaultScope); + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Information() + .CreateLogger(); + MSTestLogger.LogMessage("Class initializing..."); + if (!Directory.Exists(TestSubjectsPath)) throw new DirectoryNotFoundException(TestSubjectsPath); + } + + [TestMethod] + public void DPRARExtractorTest() + { + var l = Mock.Of(); + var f = new RARFactory(); + var e = new DPRARExtractor(l, f); + Assert.AreEqual(l, e.Logger); + Assert.AreEqual(f, e.Factory); + } + + [TestCleanup] + public void Cleanup() + { + MSTestLogger.LogMessage("Cleaning up..."); + Directory.Delete(ExtractPath, true); + } + + [TestInitialize] + public void Initialize() + { + MSTestLogger.LogMessage("Initializing..."); + Directory.CreateDirectory(ExtractPath); + Console.WriteLine(Environment.CurrentDirectory); + Console.WriteLine(Directory.Exists(TestSubjectsPath)); + } + + // This is different than the one from DPArchiveTestHelpers. + public static void SetupTargetPathsForTemp(DPArchive arc, string basePath) + { + foreach (var file in arc.Contents.Values) + file.TargetPath = Path.Combine(basePath, Path.GetFileNameWithoutExtension(arc.FileName), file.Path); + } + + [TestMethod] + [DataRow("Test.rar"), DataRow("Test_split.part1.rar"), DataRow("Test_split_solid.part1.rar")] + public void ExtractTest(string path) + { + List fileData = new(65536); + var factory = new Mock(); + using var r2 = new RAR(Path.Combine(TestSubjectsPath, path)); + RAR.DataAvailableHandler fileDataFunc = (s, e) => + { + if (!s.CurrentFile.FileName.Contains("random_image") && !path.Contains("Test_split")) return; + fileData.AddRange(e.Data); + }; + r2.DataAvailable += fileDataFunc; + factory.SetupSequence(x => x.Create(It.IsAny())).Returns(new RAR(Path.Combine(TestSubjectsPath, path))).Returns(r2); + var e = new DPRARExtractor(Log.Logger, factory.Object); + var fi = FileSystem.CreateFileInfo(Path.Combine(TestSubjectsPath, path)); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + + //// Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + [DataRow("Test.rar")] + public void ExtractTest_PartialExtract(string path) + { + var e = new DPRARExtractor(); + var fi = FileSystem.CreateFileInfo(Path.Combine(TestSubjectsPath, path)); + var arc = new DPArchive(fi); + arc.PeekContents(); + var skippedFile = arc.Contents.Values.ElementAt(1); + var successFiles = arc.Contents.Values.Except(new[] { skippedFile }).ToList(); + var settings = new DPExtractSettings(ExtractPath, successFiles); + var expectedReport = new DPExtractionReport() { ExtractedFiles = successFiles, ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + + //// Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(successFiles); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_AfterExtract() + { + var e = new DPRARExtractor(); + var fi = FileSystem.CreateFileInfo(Path.Combine(TestSubjectsPath, "Test.rar")); + var arc = new DPArchive(fi); + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + e.Extract(settings); + + //// Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtractTemp() + { + var e = new DPRARExtractor(); + var fi = FileSystem.CreateFileInfo(Path.Combine(TestSubjectsPath, "Test.rar")); + var arc = new DPArchive(fi); + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + e.ExtractToTemp(settings); + + //// Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, arc.Contents.Values.Select(x => x.Path)); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + + [TestMethod] + public void ExtractToTempTest() + { + var e = new DPRARExtractor(); + var fi = FileSystem.CreateFileInfo(Path.Combine(TestSubjectsPath, "Test.rar")); + var arc = new DPArchive(fi); + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + SetupTargetPathsForTemp(arc, ExtractPath); + + //// Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings, true); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, arc.Contents.Values.Select(x => x.Path)); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + + } + [TestMethod] + public void ExtractToTempTest_AfterExtract() + { + var e = new DPRARExtractor(); + var fi = FileSystem.CreateFileInfo(Path.Combine(TestSubjectsPath, "Test.rar")); + var arc = new DPArchive(fi); + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + SetupTargetPathsForTemp(arc, ExtractPath); + e.ExtractToTemp(settings); + + //// Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings, true); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, arc.Contents.Values.Select(x => x.Path)); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + [DataRow("Test.rar"), DataRow("Test_split.part1.rar"), DataRow("Test_split_solid.part1.rar")] + + public void PeekTest(string path) + { + var e = new DPRARExtractor(); + var fi = FileSystem.CreateFileInfo(Path.Combine(TestSubjectsPath, path)); + var arc = new DPArchive(fi); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, arc.Contents.Values.Select(x => x.Path)); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/Extraction/Integration/DPZipExtractorTests.cs b/src/DAZ_Installer.CoreTests/Extraction/Integration/DPZipExtractorTests.cs new file mode 100644 index 0000000..809ee43 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/Integration/DPZipExtractorTests.cs @@ -0,0 +1,230 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using Moq; +using Serilog; +using DAZ_Installer.IO.Fakes; +using DAZ_Installer.IO; +using DAZ_Installer.CoreTests.Extraction; +using DAZ_Installer.Core.Extraction.Fakes; +using DAZ_Installer.Core.Integration; +using System.IO; +using System.IO.Compression; + +#pragma warning disable CS0618 // Obsolete is for production code, not testing code. +namespace DAZ_Installer.Core.Extraction.Integration.Tests +{ + [TestClass] + public class DPZipExtractorTests + { + public static readonly string TempPath = Path.Combine(Path.GetTempPath(), "DAZ_Installer.CoreTests", "Extraction", "Integration", "Test Subjects"); + public static readonly string ArchivePath = Path.Combine(TempPath, "Test Archive.zip"); + public static readonly string ArchiveContentsPath = Path.Combine(TempPath, "Archive Contents"); + public static readonly string ExtractPath = Path.Combine(Path.GetTempPath(), "DAZ_InstallerTests", "Extract"); + public static readonly DPFileScopeSettings DefaultScope = new(Enumerable.Empty(), new[] { ExtractPath }, false); + public static readonly DPFileSystem FileSystem = new DPFileSystem(DefaultScope); + public static List ArchiveContents = new(5); + + /// + /// A factory that returns a mocked fake archive with the default contents. + /// + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .CreateLogger(); + ArchiveContents = DPIntegrationArchiveHelpers.CreateArchiveContents(ArchiveContentsPath); + ZipFile.CreateFromDirectory(ArchiveContentsPath, ArchivePath, CompressionLevel.NoCompression, false); + } + + [ClassCleanup] + public static void ClassCleanup() + { + Directory.Delete(TempPath, true); + Directory.Delete(ExtractPath, true); + } + + [TestCleanup] + public void TestCleanup() + { + Directory.Delete(ExtractPath, true); + } + + [TestInitialize] + public void TestInitialize() + { + Directory.CreateDirectory(ExtractPath); + } + + [TestMethod] + public void DPZipExtractorTest() + { + var l = Mock.Of(); + var f = Mock.Of(); + var e = new DPZipExtractor(l, f); + Assert.AreEqual(l, e.Logger); + Assert.AreEqual(f, e.Factory); + } + + [TestMethod] + public void ExtractTest() + { + var fi = FileSystem.CreateFileInfo(ArchivePath); + var e = new DPZipExtractor(Log.Logger, new ZipArchiveWrapperFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_AfterExtract() + { + var fi = FileSystem.CreateFileInfo(ArchivePath); + var e = new DPZipExtractor(Log.Logger, new ZipArchiveWrapperFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + arc.Extract(settings); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_AfterExtractTemp() + { + var fi = FileSystem.CreateFileInfo(ArchivePath); + var e = new DPZipExtractor(Log.Logger, new ZipArchiveWrapperFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + arc.ExtractToTemp(settings); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_CancelledBeforeOp() + { + var fi = FileSystem.CreateFileInfo(ArchivePath); + var e = new DPZipExtractor(Log.Logger, new ZipArchiveWrapperFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + e.CancellationToken = new CancellationToken(true); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + + // Testing Extract() here: + // TODO: ExtractFinish should be called once Extracting event is emitted. + var report = e.Extract(settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_CancelledDuringOp() + { + var fi = FileSystem.CreateFileInfo(ArchivePath); + var cts = new CancellationTokenSource(); + var e = new DPZipExtractor(Log.Logger, new ZipArchiveWrapperFactory()) { CancellationToken = cts.Token }; + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + e.ExtractProgress += (_, _) => cts.Cancel(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(1) { arc.Contents.First().Value }, ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + + [TestMethod] + public void ExtractToTempTest() + { + var fi = FileSystem.CreateFileInfo(ArchivePath); + var e = new DPZipExtractor(Log.Logger, new ZipArchiveWrapperFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPathsForTemp(arc, ExtractPath); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings, true); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + + } + [TestMethod] + public void ExtractToTempTest_AfterExtract() + { + var fi = FileSystem.CreateFileInfo(ArchivePath); + var e = new DPZipExtractor(Log.Logger, new ZipArchiveWrapperFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPathsForTemp(arc, ExtractPath); + arc.ExtractToTemp(settings); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings, true); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void PeekTest() + { + var e = new DPZipExtractor(Log.Logger, new ZipArchiveWrapperFactory()); + var fi = FileSystem.CreateFileInfo(ArchivePath); + var arc = new DPArchive(fi); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, arc.Contents.Values.Select(x => x.Path)); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/Helpers/DPDestinationDeterminerTestHelpers.cs b/src/DAZ_Installer.CoreTests/Helpers/DPDestinationDeterminerTestHelpers.cs new file mode 100644 index 0000000..890bf0e --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Helpers/DPDestinationDeterminerTestHelpers.cs @@ -0,0 +1,80 @@ +using DAZ_Installer.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DAZ_Installer.Core.Tests.Helpers +{ + internal static class DPDestinationDeterminerTestHelpers + { + public static void AssertDestinations(HashSet expected, HashSet actual, string basePath) + { + Assert.AreEqual(expected.Count, actual.Count, "Destinations' count are not the same."); + foreach (var file in expected) + { + Assert.IsTrue(actual.Contains(file), "Actual destinations does not contain expected file."); + } + } + + /// + /// Asserts that all files in the archive have the correct s. + /// + /// The actual determined destinations from + /// + /// + /// The expected paths to be + public static void AssertTargetPaths(HashSet actual, IEnumerable expectedPaths) + { + var expectedHashSet = new HashSet(expectedPaths.Select(x => PathHelper.NormalizePath(x))); + foreach (var file in actual) + { + Assert.IsTrue(expectedHashSet.Contains(PathHelper.NormalizePath(file.TargetPath)), $"File {file.FileName} has incorrect TargetPath."); + } + } + + /// + /// Asserts that all files in the archive have the correct s + /// and paths. + /// + /// The archive to check. + /// The expected RelativeTargetPaths. + /// The expected RelativePathToContentFolder paths. + public static void AssertRelativePaths(DPArchive arc, IEnumerable expectedRTPaths, IEnumerable expectedRPTCFPaths) + { + var normalizedExpectedRTPaths = expectedRTPaths.Select(x => PathHelper.NormalizePath(x)); + var normalizedExpectedRPTCFPaths = expectedRPTCFPaths.Select(x => PathHelper.NormalizePath(x)); + var foundRTs = new HashSet(arc.Contents.Values.Select(x => x.RelativeTargetPath)); + var foundRPTCFs = new HashSet(arc.Contents.Values.Select(x => x.RelativePathToContentFolder)); + + foreach (var path in normalizedExpectedRTPaths) + { + Assert.IsTrue(foundRTs.Contains(path), $"Expected RelativeTargetPath {path} is not found."); + } + foreach (var path in normalizedExpectedRPTCFPaths) + { + Assert.IsTrue(foundRPTCFs.Contains(path), $"Expected RelativePathToContentFolder {path} is not found."); + } + } + + /// + /// Asserts that all files in the archive have the correct s + /// + /// The archive to check. + /// The expected absolute content folders paths. + public static void AssertContentFolders(DPArchive arc, IEnumerable expectedContentFolderPaths) + { + var expectedContentFolderHashSet = new HashSet(expectedContentFolderPaths.Select(x => PathHelper.NormalizePath(x))); + var contentFolders = arc.Folders.Values.Where(f => f.IsContentFolder); + Assert.AreEqual(expectedContentFolderHashSet.Count, contentFolders.Count(), "Content folder count is not the same."); + foreach (var folder in contentFolders) + { + Assert.IsTrue(expectedContentFolderHashSet.Contains(folder.NormalizedPath), $"Folder {folder.NormalizedPath} is not marked as content folder."); + } + } + + + } +} diff --git a/src/DAZ_Installer.CoreTests/Helpers/DPProcessorTestHelpers.cs b/src/DAZ_Installer.CoreTests/Helpers/DPProcessorTestHelpers.cs new file mode 100644 index 0000000..10e77a7 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Helpers/DPProcessorTestHelpers.cs @@ -0,0 +1,193 @@ +using DAZ_Installer.Core.Extraction; +using DAZ_Installer.CoreTests.Extraction; +using DAZ_Installer.IO; +using DAZ_Installer.IO.Fakes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Serilog; +using System.Text; +using System.Text.Json; +using System.Xml; + +namespace DAZ_Installer.Core.Tests +{ + [Obsolete("For testing purposes only.")] + internal class DPProcessorTestHelpers + { + /// + /// Contains the DefaultContents from and the manifest and supplement."/> + /// + public static IEnumerable DefaultContents => new string[] { "Manifest.dsx", "Supplement.dsx", "Contents/a.txt", "Contents/b.txt", "Contents/Documents/c.png", "Contents/Documents/d.txt", "Contents/e.duf", "Contents/f.duf", "bullshit.png" }; + public static MockOptions DefaultMockOptions => new(); + public static AssertOptions DefaultAssertOptions => new(); + public struct MockOptions + { + public bool partialFileInfo = true; + public bool partialRAR = true; + public bool partialDPFileInfo = true; + public bool partialZipArchiveEntry = true; + public bool partialFakeFileSystem = true; + public IEnumerable paths = DefaultContents; + public Func? ExtractToTempFunc = null; + public Func? ExtractFunc = null; + + public MockOptions() { } + } + public static DPArchive NewMockedArchive(MockOptions options, out Mock extractor, out Mock fakeDPFileInfo, out Mock fakeFileInfo, out Mock fakeFileSystem) + { + var fs = new Mock() { CallBase = options.partialFakeFileSystem }; + fakeFileSystem = fs; + fakeFileInfo = new Mock("Z:/test.rar") { CallBase = options.partialFileInfo }; + fakeDPFileInfo = new Mock(fakeFileInfo.Object, fakeFileSystem.Object, null) { CallBase = options.partialDPFileInfo }; + extractor = new Mock(); + var arc = new DPArchive(string.Empty, Log.Logger.ForContext(), fakeDPFileInfo.Object, extractor.Object); + extractor.Setup(x => x.ExtractToTemp(It.IsAny())).Returns((DPExtractSettings x) => + { + if (options.ExtractToTempFunc is null) return handleExtract(x, fs.Object); + return options.ExtractToTempFunc(); + }); + extractor.Setup(x => x.Extract(It.IsAny())).Returns((DPExtractSettings x) => + { + if (options.ExtractFunc is null) return handleExtract(x, fs.Object); + return options.ExtractFunc(); + }); + return arc; + } + + public static DPProcessor SetupProcessor(DPArchive arc, FakeFileSystem system, out Mock destDerm, out Mock tagProvider) + { + var d = destDerm = new Mock(); + d.Setup(x => x.DetermineDestinations(It.IsAny(), It.IsAny())).Returns(() => arc.Contents.Values.ToHashSet()); + var t = tagProvider = new Mock(); + var p = new DPProcessor() + { + Logger = Log.Logger.ForContext(), + FileSystem = system, + DestinationDeterminer = d.Object, + TagProvider = t.Object, + }; + SetupEntities(DefaultContents, arc); + UpdateFileInfos(new DPExtractSettings("A:/", arc.Contents.Values, archive: arc), system); + return p; + } + + public static void AssertCommon(DPProcessor processor, Times? time = null) + { + var times = time is not null ? time.Value : Times.Once(); + Mock.Get(processor.DestinationDeterminer).Verify(x => x.DetermineDestinations(It.IsAny(), It.IsAny()), times); + Mock.Get(processor.TagProvider).Verify(x => x.GetTags(It.IsAny(), It.IsAny()), times); + } + + private static DPExtractionReport handleExtract(DPExtractSettings settings, FakeFileSystem fs) + { + UpdateFileInfos(settings, fs); + return new DPExtractionReport() + { + ErroredFiles = new(0), + ExtractedFiles = settings.FilesToExtract.ToList(), + Settings = settings + }; + } + + private static void SetupEntities(IEnumerable paths, DPArchive arc) + { + foreach (var path in paths) + { + if (string.IsNullOrEmpty(Path.GetFileName(path))) new DPFolder(path, arc, null); + else DPFile.CreateNewFile(path, arc, null); + } + } + + private static void UpdateFileInfos(DPExtractSettings settings, FakeFileSystem system) + { + foreach (var file in settings.Archive.Contents.Values) + { + var path = string.IsNullOrEmpty(file.TargetPath) ? Path.Combine(settings.TempPath, file.Path) : file.TargetPath; + file.FileInfo = system.CreateFileInfo(path); + var mockFileInfo = Mock.Get(file.FileInfo); + var stream = DPArchiveTestHelpers.DetermineFileStream(file, settings.Archive); + Exception? ex = null; + mockFileInfo.Setup(x => x.TryAndFixOpenRead(out It.Ref.IsAny, out ex)) + .Callback((out Stream s, out Exception ex) => + { + s = DPArchiveTestHelpers.DetermineFileStream(file, settings.Archive); + ex = null; + }) + .Returns(true); + } + } + + public struct AssertOptions + { + public int ExpectedProcessErrorCount = 0; + public int ExpectedArchiveCount = 1; + public int ExpectedFileErrorCount = 0; + public Dictionary? ExpectArchiveProcessed = null; + + public AssertOptions() { } + } + + public static void AttachCommonEventHandlers(DPProcessor processor, AssertOptions opts) + { + int arcEnterCount = 0, arcExitCount = 0; + int processErrorCount = 0; + processor.ArchiveEnter += (_, e) => + { + if (++arcEnterCount > opts.ExpectedArchiveCount) Assert.Fail("Archive Enter called more than expected"); + }; + processor.ArchiveExit += (_, e) => + { + arcExitCount++; + if (opts.ExpectArchiveProcessed is null) return; + if (!opts.ExpectArchiveProcessed.TryGetValue(e.Archive.FileName, out var wantReport)) return; + if (e.Report is null) Log.Logger.Warning("Report is null"); + else AssertReport(wantReport, e.Report); + }; + processor.ProcessError += (_, e) => + { + if (++processErrorCount > opts.ExpectedProcessErrorCount) Assert.Fail("Process Error called more than expected"); + }; + processor.Finished += () => + { + if (arcEnterCount != arcExitCount) Assert.Fail("Archive Enter and Exit counts do not match"); + }; + } + + public static DPExtractionReport CreateExtractionReport(DPExtractSettings settings, IEnumerable failedFiles, IEnumerable? successFiles) + { + var fh = failedFiles.ToHashSet(); + var sh = successFiles?.ToHashSet() ?? new HashSet(); + var extractedFiles = successFiles?.ToList() ?? settings.Archive.Contents.Values.Where(m => !fh.Contains(m.Path)).Select(x => x.Path); + if (fh.Intersect(sh).Count() > 1) Assert.Inconclusive("Failed and Success files intersect"); + return new DPExtractionReport() + { + Settings = settings, + ErroredFiles = failedFiles.ToDictionary(m => CreateDummyFile(m), _ => string.Empty), + ExtractedFiles = successFiles?.Select(x => new DPFile(x, null, null, null, null!)).ToList() ?? new List() + }; + } + + public static DPFile CreateDummyFile(string path) => new(path, null, null, null, null!); + public static DPFolder CreateDummyFolder(string path) => new(path, null, null!); + + public static void AssertReport(DPExtractionReport want, DPExtractionReport got) + { + var a = want.ErroredFiles.Keys.Select(x => x.Path).ToArray(); + var b = got.ErroredFiles.Keys.Select(x => x.Path).ToArray(); + CollectionAssert.AreEqual(a, b, "Errored file paths are not equal"); + a = want.ExtractedFiles.Select(x => x.Path).ToArray(); + b = got.ExtractedFiles.Select(x => x.Path).ToArray(); + CollectionAssert.AreEqual(a, b, "Extracted file paths are not equal"); + + // Settings + Assert.AreSame(want.Settings.Archive, got.Settings.Archive, "Reports' archive are not the same"); + CollectionAssert.AreEqual(want.Settings.FilesToExtract.Select(x => x.Path).ToArray(), got.Settings.FilesToExtract.Select(x => x.Path).ToArray(), "Reports' files to extract are not the same"); + Assert.AreEqual(want.Settings.OverwriteFiles, got.Settings.OverwriteFiles, "Reports' overwrite files are not the same"); + } + + public static List CalculateExpectedFiles(IEnumerable files) => files.Where(x => !string.IsNullOrEmpty(Path.GetFileName(x))).ToList(); + + public static DPExtractSettings CreateExtractSettings(IEnumerable paths, DPArchive arc) => new("A:/", paths.Where(x => !string.IsNullOrEmpty(Path.GetFileName(x))).Select(x => CreateDummyFile(x)), archive: arc); + + } +} diff --git a/src/DAZ_Installer.CoreTests/Integration/DPIntegrationArchiveHelpers.cs b/src/DAZ_Installer.CoreTests/Integration/DPIntegrationArchiveHelpers.cs new file mode 100644 index 0000000..a178884 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Integration/DPIntegrationArchiveHelpers.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DAZ_Installer.Core.Integration +{ + internal class DPIntegrationArchiveHelpers + { + internal const string ManifestContent = + @" + + + + + "; + + internal const string SupplementContent = + @" + + + + "; + + internal const string DSFContent = + @"{ + ""file_version"" : ""0.6.0.0"", + ""asset_info"" : { + ""id"" : ""/data/data.dsf"", + ""type"" : ""prop"", + ""contributor"" : { + ""author"" : ""TheRealSolly"", + ""email"" : ""solomon1blount@gmail.com"", + ""website"" : ""www.thesolomonchronicles.com"" + }, + ""revision"" : ""1.0"", + ""modified"" : ""2020-12-06T00:04:11Z"" + } + }"; + /// + /// Creates a dummy file at the with random bytes. + /// + /// The path to save the dummy file to. + /// The number of random bytes to save to file. + /// " + public static void CreateDummyFile(string path, uint n) + { + //uint n = 10 * (uint)Math.Pow(2, 20); // 10 MB. + uint l = n / 65536; + var bytes = new byte[n / l]; + using var file = File.Create(path); + for (var i = 0; i < l; i++) + { + new Random().NextBytes(bytes); + file.Write(bytes, 0, bytes.Length); + } + } + + public static void CreateManifestFile(string path) + { + using var file = File.CreateText(path); + file.Write(ManifestContent); + file.Close(); + } + + public static void CreateSupplementFile(string path) + { + using var file = File.CreateText(path); + file.Write(SupplementContent); + file.Close(); + } + + public static void CreateDSFFile(string path) + { + using var file = File.CreateText(path); + file.Write(DSFContent); + file.Close(); + } + + public static List CreateArchiveContents(string basePath) + { + // File structure + // basePath + // - data + // - TheRealSolly + // - data.dsf + // - a.txt + // - docs + // - b.txt + // - not included in content folders + // - this should not be processed.txt + // - random_image_for_splitting.jpg + + var dataPath = Path.Combine(basePath, "data"); + var docsPath = Path.Combine(basePath, "docs"); + Directory.CreateDirectory(basePath); + Directory.CreateDirectory(dataPath); + Directory.CreateDirectory(docsPath); + var notIncludedPath = Path.Combine(basePath, "not included in content folders"); + Directory.CreateDirectory(notIncludedPath); + var snbpFile = Path.Combine(notIncludedPath, "this should not be processed.txt"); + File.Create(snbpFile).Close(); + var imgFile = Path.Combine(notIncludedPath, "random_image_for_splitting.jpg"); + CreateDummyFile(imgFile, 10 * (uint)Math.Pow(2, 20)); + var bFile = Path.Combine(docsPath, "b.txt"); + File.Create(bFile).Close(); + var theRealSollyPath = Path.Combine(dataPath, "TheRealSolly"); + Directory.CreateDirectory(theRealSollyPath); + var aFile = Path.Combine(theRealSollyPath, "a.txt"); + File.Create(aFile).Close(); + var dataFile = Path.Combine(theRealSollyPath, "data.dsf"); + CreateDSFFile(dataFile); + return new List() { snbpFile, bFile, aFile, dataFile, imgFile }; + } + + public static void CreateDAZArchiveContents(string basePath) + { + // File structure + // basePath + // - manifest.dsx + // - supplement.dsx + // - random_image_for_splitting.jpg + // - Content + // - data + // - TheRealSolly + // - data.dsf + // - a.txt + // - docs + // - b.txt + // - not included in content folders + // - this should not be processed.txt + + var contentPath = Path.Combine(basePath, "Content"); + var dataPath = Path.Combine(contentPath, "data"); + var docsPath = Path.Combine(contentPath, "docs"); + Directory.CreateDirectory(contentPath); + Directory.CreateDirectory(dataPath); + Directory.CreateDirectory(docsPath); + var notIncludedPath = Path.Combine(basePath, "not included in content folders"); + Directory.CreateDirectory(notIncludedPath); + File.Create(Path.Combine(notIncludedPath, "this should not be processed.txt")).Close(); + File.Create(Path.Combine(docsPath, "b.txt")).Close(); + var theRealSollyPath = Path.Combine(dataPath, "TheRealSolly"); + Directory.CreateDirectory(theRealSollyPath); + File.Create(Path.Combine(theRealSollyPath, "a.txt")).Close(); + CreateManifestFile(Path.Combine(basePath, "manifest.dsx")); + CreateSupplementFile(Path.Combine(basePath, "supplement.dsx")); + CreateDSFFile(Path.Combine(theRealSollyPath, "data.dsf")); + } + public static void AssertDefaultContentsNonDAZ(DPArchive arc) + { + Assert.AreEqual(5, arc.Contents.Count, "Archive contents count does not match"); + Assert.AreEqual(3, arc.RootFolders.Count, "Archive root folders count does not match"); + Assert.AreEqual(4, arc.Folders.Count, "Archive folders count does not match"); + Assert.AreEqual(0, arc.RootContents.Count, "Archive root contents count does not match"); + } + public static void AssertDefaultContentsDAZ(DPArchive arc) + { + Assert.AreEqual(7, arc.Contents.Count, "Archive contents count does not match"); + Assert.AreEqual(3, arc.RootFolders.Count, "Archive root folders count does not match"); + Assert.AreEqual(5, arc.Folders.Count, "Archive folders count does not match"); + Assert.AreEqual(3, arc.RootContents.Count, "Archive root contents count does not match"); + } + + } +} diff --git a/src/DAZ_Installer.CoreTests/Integration/DPProcessorTests.cs b/src/DAZ_Installer.CoreTests/Integration/DPProcessorTests.cs new file mode 100644 index 0000000..4357620 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Integration/DPProcessorTests.cs @@ -0,0 +1,73 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using Serilog; +using DAZ_Installer.Core.Extraction; +using Moq; +using DAZ_Installer.IO.Fakes; +using DAZ_Installer.IO; +using DAZ_Installer.Core.Tests; +using System.IO.Compression; + +#pragma warning disable 618 +namespace DAZ_Installer.Core.Integration.Tests +{ + [TestClass] + public class DPProcessorTests + { + public static readonly string TempPath = Path.Combine(Path.GetTempPath(), "DAZ_Installer.CoreTests", "Integration"); + public static readonly string ArchivePath = Path.Combine(TempPath, "Test Archive.zip"); + public static readonly string ArchiveContentsPath = Path.Combine(TempPath, "Archive Contents"); + public static readonly string ExtractPath = Path.Combine(TempPath, "Extract"); + public static readonly DPFileScopeSettings DefaultScope = new(Enumerable.Empty(), new[] { ExtractPath }, false); + public static readonly DPFileSystem FileSystem = new DPFileSystem(DefaultScope); + public static List ArchiveContents = new(5); + static readonly DPProcessSettings DefaultProcessSettings = new(TempPath, ExtractPath, InstallOptions.ManifestAndAuto); + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Information() + .CreateLogger(); + ArchiveContents = DPIntegrationArchiveHelpers.CreateArchiveContents(ArchiveContentsPath); + ZipFile.CreateFromDirectory(ArchiveContentsPath, ArchivePath, CompressionLevel.NoCompression, false); + } + + [ClassCleanup] + public static void ClassCleanup() + { + Directory.Delete(TempPath, true); + Directory.Delete(ExtractPath, true); + } + + [TestCleanup] + public void TestCleanup() + { + Directory.Delete(ExtractPath, true); + } + + [TestInitialize] + public void TestInitialize() + { + Directory.CreateDirectory(ExtractPath); + } + + [TestMethod] + public void ProcessArchiveTest() + { + var a = new DPArchive(FileSystem.CreateFileInfo(ArchivePath)); + var p = new DPProcessor(); + var settings = DPProcessorTestHelpers.CreateExtractSettings(ArchiveContents, a); + var expectedFiles = new[] { "data/TheRealSolly/data.dsf", "data/TheRealSolly/a.txt", "docs/b.txt" }; + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectArchiveProcessed = new() { { a.FileName, DPProcessorTestHelpers.CreateExtractionReport(settings, Enumerable.Empty(), expectedFiles) } } + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + + p.ProcessArchive(a, DefaultProcessSettings); + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test.rar b/src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test.rar new file mode 100644 index 0000000..fe54449 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test.rar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d53f62f6e8391f2270c64fc4a2b9b655d71cadc6a6f1aff8c0bb9dd9a48976bc +size 19893360 diff --git a/src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test.zip b/src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test.zip new file mode 100644 index 0000000..904edc0 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9f386b68487214b8178a9e1b8718e0860fe6e62a53bd700e1962f58329e98eb +size 20354581 diff --git a/src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split.part1.rar b/src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split.part1.rar new file mode 100644 index 0000000..bbe37c1 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split.part1.rar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5496804c80e1748cf9b30fb21444cb39921bca2455050a7cc87976cf0a9aec3e +size 15728640 diff --git a/src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split.part2.rar b/src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split.part2.rar new file mode 100644 index 0000000..6ea79e3 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split.part2.rar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48692c5cf90981402e898a66e9b42da2611c5867d3627771baae22cbda377bd2 +size 4164944 diff --git a/src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split_solid.part1.rar b/src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split_solid.part1.rar new file mode 100644 index 0000000..50aff05 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split_solid.part1.rar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d7ae82e1a96f1259fbd94e543cd71cb93c6a4ecf04bd871818bd67c5b88db1e +size 15728640 diff --git a/src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split_solid.part2.rar b/src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split_solid.part2.rar new file mode 100644 index 0000000..99ce588 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split_solid.part2.rar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0386eac4fd9d974161478406965c6ed6415519f4be8e380df1a400dc409ef53d +size 4164871 diff --git a/src/DAZ_Installer.CoreTests/RealData/Archives/--INSERT YOUR ARCHIVES HERE--.txt b/src/DAZ_Installer.CoreTests/RealData/Archives/--INSERT YOUR ARCHIVES HERE--.txt new file mode 100644 index 0000000..b31599d --- /dev/null +++ b/src/DAZ_Installer.CoreTests/RealData/Archives/--INSERT YOUR ARCHIVES HERE--.txt @@ -0,0 +1 @@ +Insert your archives here. You may also use symbolic links. \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/RealData/DPProcessorTests.cs b/src/DAZ_Installer.CoreTests/RealData/DPProcessorTests.cs new file mode 100644 index 0000000..080075f --- /dev/null +++ b/src/DAZ_Installer.CoreTests/RealData/DPProcessorTests.cs @@ -0,0 +1,81 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using Serilog; +using DAZ_Installer.Core.Extraction; +using Moq; +using DAZ_Installer.IO.Fakes; +using DAZ_Installer.IO; +using DAZ_Installer.Core.Integration; +using System.IO.Compression; +using DAZ_Installer.Core.Tests.RealData; + +#pragma warning disable 618 +namespace DAZ_Installer.Core.RealData.Tests +{ + /// + /// This class uses real archives/products such as from the DAZ store, to make sure that the DPProcessor works correctly. It uses the "manifests" that was generated from + /// the project/tool to determine which files should be moved into the user's library. + /// In order to run these tests, you must have the DAZ_Installer.TestingSuiteWindows project/tool built and run it to generate the manifests. + /// Then you must copy the manifests into the DAZ_Installer.CoreTests/RealData/Manifests folder. + /// To protect the copyrights of authors, please do NOT upload the archives online. Test offline with your products. + /// + [TestClass] + public class DPProcessorTests + { + public static readonly string TempPath = Path.Combine(Path.GetTempPath(), "DAZ_Installer.CoreTests", "RealDataDir"); + public static readonly string ArchivesPath = Path.Combine(Environment.CurrentDirectory, "RealData", "Archives"); + public static readonly string ManifestsPath = Path.Combine(Environment.CurrentDirectory, "RealData", "Manifests"); + public static readonly string ExtractPath = Path.Combine(TempPath, "Extract"); + public static readonly DPFileScopeSettings DefaultScope = new(Enumerable.Empty(), new[] { Path.GetTempPath(), TempPath, ExtractPath }, false); + public static readonly DPFileSystem FileSystem = new(DefaultScope); + public static List Manifests = RealDataHelper.GetValidTestCases(ArchivesPath, ManifestsPath); + public TestContext TestContext { get; set; } = null!; + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Information() + .CreateLogger(); + Manifests = RealDataHelper.GetValidTestCases(ArchivesPath, ManifestsPath); + } + + [TestCleanup] + public void TestCleanup() + { + try + { + FileSystem.DeleteDirectory(TempPath, true); + } + catch { } + } + + [TestMethod] + public void ProcessArchiveTest() + { + foreach (var manifest in Manifests) + { + var arcPath = Path.Combine(ArchivesPath, manifest.ArchiveName); + var a = new DPArchive(FileSystem.CreateFileInfo(arcPath)); + var p = new DPProcessor() { CancellationToken = TestContext.CancellationTokenSource.Token }; + var exitArgs = new List(); + p.ArchiveExit += (_, a) => exitArgs.Add(a); + p.ArchiveExit += (_, a) => + { + if (!a.Processed) p.CancelProcessing(); + Assert.Fail($"Archive processing for {a.Archive.FileName} failed"); + }; + + var processSettings = manifest.Settings with { TempPath = TempPath, DestinationPath = ExtractPath }; + p.ProcessArchive(a, processSettings); + + RealDataHelper.AssertProcess(exitArgs, manifest); + Log.Information("{Archive} passed.", manifest.ArchiveName); + + } + + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/RealData/Manifests/--INSERT YOUR MANIFESTS HERE--.txt b/src/DAZ_Installer.CoreTests/RealData/Manifests/--INSERT YOUR MANIFESTS HERE--.txt new file mode 100644 index 0000000..4e09b8c --- /dev/null +++ b/src/DAZ_Installer.CoreTests/RealData/Manifests/--INSERT YOUR MANIFESTS HERE--.txt @@ -0,0 +1 @@ +Use this folder to insert the corresponding manifests to test. A manifest is a JSON file (a file with the .json extension). \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/RealData/RealDataHelper.cs b/src/DAZ_Installer.CoreTests/RealData/RealDataHelper.cs new file mode 100644 index 0000000..f614f96 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/RealData/RealDataHelper.cs @@ -0,0 +1,55 @@ +using DAZ_Installer.Core.Extraction; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DAZ_Installer.Core.Tests.RealData +{ + internal static class RealDataHelper + { + public static List GetManifests(string manifestDir) + { + return Directory.EnumerateFiles(manifestDir, "*.json") + .Select(file => DPProcessorTestManifest.FromJson(File.ReadAllText(file))) + .ToList(); + } + + public static List GetArchives(string archiveDir) => Directory.EnumerateFiles(archiveDir) + .Where(x => x.EndsWith(".zip") + || x.EndsWith(".rar") + || x.EndsWith(".7z") + || x.EndsWith(".001")) + .Select(x => Path.GetFileName(x)).ToList(); + + public static List GetValidTestCases(string archiveDir, string manifestDir) + { + try + { + var a = new HashSet(GetArchives(archiveDir)); + var m = GetManifests(manifestDir); + var t = new List(); + + foreach (var manifest in m) + { + if (a.Contains(manifest.ArchiveName)) t.Add(manifest); + } + return t; + } catch { return new(); } + + } + + public static void AssertProcess(List args, DPProcessorTestManifest manifest) + { + foreach (var report in args) + { + Assert.IsNotNull(report.Report); + var h = report.Report.ExtractedFiles.ToDictionary(x => x.Path, x => x.RelativePathToContentFolder); + var map = manifest.Results.Find(x => x.ArchiveName == report.Archive.FileName).Mappings; + foreach (var mapping in map) + { + Assert.IsTrue(h.ContainsKey(mapping.Key)); + // Assert whether the relative target paths are the same. + Assert.AreEqual(h[mapping.Key], mapping.Value); + } + } + } + } +} diff --git a/src/DAZ_Installer.Database/DAZ_Installer.Database.csproj b/src/DAZ_Installer.Database/DAZ_Installer.Database.csproj new file mode 100644 index 0000000..fba5cf6 --- /dev/null +++ b/src/DAZ_Installer.Database/DAZ_Installer.Database.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/src/DP/DPDatabase.Abstraction.cs b/src/DAZ_Installer.Database/DPDatabase.Abstraction.cs similarity index 76% rename from src/DP/DPDatabase.Abstraction.cs rename to src/DAZ_Installer.Database/DPDatabase.Abstraction.cs index 4f28be6..bc477ad 100644 --- a/src/DP/DPDatabase.Abstraction.cs +++ b/src/DAZ_Installer.Database/DPDatabase.Abstraction.cs @@ -1,16 +1,13 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE -using System; +using Serilog; using System.Data; -using System.Collections.Generic; -using System.Text; -using System.Threading; using System.Data.SQLite; -using System.IO; +using System.Text; -namespace DAZ_Installer.DP +namespace DAZ_Installer.Database { - public static partial class DPDatabase + public partial class DPDatabase { #region Reads /// @@ -18,20 +15,20 @@ public static partial class DPDatabase /// /// A connection to reuse, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). - private static void UpdateProductRecordCount(SQLiteConnection? connection, CancellationToken t) + private void UpdateProductRecordCount(SQLiteConnection? connection, CancellationToken t) { const string getCmd = @"SELECT ""Product Record Count"" FROM DatabaseInfo;"; if (t.IsCancellationRequested) return; try { - using (var cmd = new SQLiteCommand(getCmd, connection)) - ProductRecordCount = Convert.ToUInt32(cmd.ExecuteScalar()); + using SQLiteCommand cmd = new(getCmd, connection); + ProductRecordCount = Convert.ToUInt32(cmd.ExecuteScalar()); } catch (Exception e) { - DPCommon.WriteToLog($"An unexpected error occurred while attempting to get product record count. REASON: {e}"); + Logger.ForContext().Error(e, "An unexpected error occurred while attempting to get product record count."); } - DPCommon.WriteToLog("Product Record Count: ", ProductRecordCount); + // DPCommon.WriteToLog("Product Record Count: ", ProductRecordCount); } /// @@ -39,19 +36,19 @@ private static void UpdateProductRecordCount(SQLiteConnection? connection, Cance /// /// A connection to reuse, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). - private static void UpdateExtractionRecordCount(SQLiteConnection? connection, CancellationToken t) + private void UpdateExtractionRecordCount(SQLiteConnection? connection, CancellationToken t) { const string getCmd = @"SELECT ""Extraction Record Count"" FROM DatabaseInfo;"; try { - using (var cmd = new SQLiteCommand(getCmd, connection)) - ExtractionRecordCount = Convert.ToUInt32(cmd.ExecuteScalar()); + using SQLiteCommand cmd = new(getCmd, connection); + ExtractionRecordCount = Convert.ToUInt32(cmd.ExecuteScalar()); } catch (Exception e) { - DPCommon.WriteToLog($"An unexpected error occurred while attempting to get extraction record count. REASON: {e}"); + // DPCommon.WriteToLog($"An unexpected error occurred while attempting to get extraction record count. REASON: {e}"); } - DPCommon.WriteToLog("Extraction Record Count: ", ExtractionRecordCount); + // DPCommon.WriteToLog("Extraction Record Count: ", ExtractionRecordCount); } /// @@ -61,11 +58,11 @@ private static void UpdateExtractionRecordCount(SQLiteConnection? connection, Ca /// The command that is ready to execute. Cannot be null. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). /// An array of product records found from search. - private static DPProductRecord[] SearchProductRecordsViaTagsS(SQLiteCommand command, CancellationToken t) + private DPProductRecord[] SearchProductRecordsViaTagsS(SQLiteCommand command, CancellationToken t) { if (t.IsCancellationRequested) return Array.Empty(); - var reader = command.ExecuteReader(); - var searchResults = new List(25); + SQLiteDataReader reader = command.ExecuteReader(); + List searchResults = new(25); string productName, author, thumbnailPath, sku; string[] tags; DateTime dateCreated; @@ -81,7 +78,7 @@ private static DPProductRecord[] SearchProductRecordsViaTagsS(SQLiteCommand comm tags = rawTags.Trim().Split(", "); author = reader["Author"] as string; // May return NULL thumbnailPath = reader["Thumbnail Full Path"] as string; // May return NULL - extractionID = Convert.ToUInt32(reader["Extraction Record ID"] is DBNull ? + extractionID = Convert.ToUInt32(reader["Extraction Record ID"] is DBNull ? 0 : reader["Extraction Record ID"]); dateCreated = DateTime.FromFileTimeUtc((long)reader["Date Created"]); sku = reader["SKU"] as string; // May return NULL @@ -99,12 +96,11 @@ private static DPProductRecord[] SearchProductRecordsViaTagsS(SQLiteCommand comm /// The table to get columns from. /// The SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). - private static string[] GetColumns(string tableName, SQLiteConnection? c, + private string[] GetColumns(string tableName, SQLiteConnection? c, CancellationToken t) { if (t.IsCancellationRequested || tableName.Length == 0) return Array.Empty(); - if (_columnsCache.ContainsKey(tableName)) return _columnsCache[tableName]; - SQLiteConnection connection = null; + SQLiteConnection? connection = null; SQLiteCommand sqlCommand = null; try { @@ -114,25 +110,24 @@ private static string[] GetColumns(string tableName, SQLiteConnection? c, var randomCommand = $"SELECT * FROM {tableName} LIMIT 1;"; sqlCommand = new SQLiteCommand(randomCommand, connection); - var reader = sqlCommand.ExecuteReader(); - var table = reader.GetSchemaTable(); + SQLiteDataReader reader = sqlCommand.ExecuteReader(); + DataTable table = reader.GetSchemaTable(); - List columns = new List(); + List columns = new(); foreach (DataRow row in table.Rows) { if (t.IsCancellationRequested) return Array.Empty(); columns.Add((string)row.ItemArray[0]); } - - // Cache it. - _columnsCache[tableName] = columns.ToArray(); - return _columnsCache[tableName]; } catch (Exception e) { - DPCommon.WriteToLog($"An unexpected error occurred attempting to get columns for table: {tableName}. REASON: {e}"); - } finally { - if (c is null) { + // DPCommon.WriteToLog($"An unexpected error occurred attempting to get columns for table: {tableName}. REASON: {e}"); + } + finally + { + if (c is null) + { connection?.Dispose(); sqlCommand?.Dispose(); } @@ -145,11 +140,11 @@ private static string[] GetColumns(string tableName, SQLiteConnection? c, /// /// The SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). - private static string[] GetTables(SQLiteConnection? c, CancellationToken cancellationToken) + private string[] GetTables(SQLiteConnection? c, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) return Array.Empty(); - var tables = new List(); - SQLiteConnection connection = null; + List tables = new(); + SQLiteConnection? connection = null; SQLiteCommand sqlCommand = null; try @@ -158,7 +153,7 @@ private static string[] GetTables(SQLiteConnection? c, CancellationToken cancell if (connection == null) return Array.Empty(); var randomCommand = $"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"; sqlCommand = new SQLiteCommand(randomCommand, connection); - using (var reader = sqlCommand.ExecuteReader()) + using (SQLiteDataReader reader = sqlCommand.ExecuteReader()) { while (reader.Read()) tables.Add(reader.GetString(0)); @@ -169,9 +164,12 @@ private static string[] GetTables(SQLiteConnection? c, CancellationToken cancell } catch (Exception e) { - DPCommon.WriteToLog($"An unexpected error occurred attempting to get table names. REASON: {e}"); - } finally { - if (c is null) { + // DPCommon.WriteToLog($"An unexpected error occurred attempting to get table names. REASON: {e}"); + } + finally + { + if (c is null) + { connection?.Dispose(); sqlCommand?.Dispose(); } @@ -186,48 +184,49 @@ private static string[] GetTables(SQLiteConnection? c, CancellationToken cancell /// The extraction record ID to fetch from database. /// The SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). - private static DPExtractionRecord? GetExtractionRecord(uint id, SQLiteConnection? c, CancellationToken t) + private DPExtractionRecord? GetExtractionRecord(uint id, SQLiteConnection? c, CancellationToken t) { if (t.IsCancellationRequested) return null; var getCmd = $"SELECT * FROM ExtractionRecords WHERE ID = {id};"; - SQLiteConnection connection = null; + SQLiteConnection? connection = null; SQLiteCommand cmd = null; try { connection = CreateAndOpenConnection(c, true); if (connection == null) return null; cmd = new SQLiteCommand(getCmd, connection); - using (var reader = cmd.ExecuteReader()) + using SQLiteDataReader reader = cmd.ExecuteReader(); + while (reader.Read()) { - while (reader.Read()) - { - string[] files, folders, erroredFiles, errorMessages; - string archiveFileName = reader["Archive Name"] as string; - string filesStr = reader["Files"] as string; - string foldersStr = reader["Folders"] as string; - string destinationPath = reader["Destination Path"] as string; - string erroredFilesStr = reader["Errored Files"] as string; - string errorMessagesStr = reader["Error Messages"] as string; - uint pid = Convert.ToUInt32(reader["Product Record ID"]); - - files = filesStr != null ? files = filesStr.Split(", ") : Array.Empty(); - folders = foldersStr != null ? folders = foldersStr.Split(", ") : Array.Empty(); - erroredFiles = erroredFilesStr != null ? erroredFiles = erroredFilesStr.Split(", ") : Array.Empty(); - errorMessages = errorMessagesStr != null ? errorMessages = erroredFilesStr.Split(", ") : Array.Empty(); - - var record = new DPExtractionRecord(archiveFileName, destinationPath, files, erroredFiles, errorMessages, folders, pid); - // RecordQueryCompleted?.Invoke(record); - return record; - } + string[] files, folders, erroredFiles, errorMessages; + var archiveFileName = reader["Archive Name"] as string; + var filesStr = reader["Files"] as string; + var foldersStr = reader["Folders"] as string; + var destinationPath = reader["Destination Path"] as string; + var erroredFilesStr = reader["Errored Files"] as string; + var errorMessagesStr = reader["Error Messages"] as string; + var pid = Convert.ToUInt32(reader["Product Record ID"]); + + files = filesStr?.Split(", ") ?? Array.Empty(); + folders = foldersStr?.Split(", ") ?? Array.Empty(); + erroredFiles = erroredFilesStr?.Split(", ") ?? Array.Empty(); + errorMessages = errorMessagesStr?.Split(", ") ?? Array.Empty(); + + DPExtractionRecord record = new(archiveFileName, destinationPath, files, erroredFiles, errorMessages, folders, pid); + // RecordQueryCompleted?.Invoke(record); + return record; } - DPCommon.WriteToLog("Failed to get extraction record possibly due to extraction record was deleted."); + // DPCommon.WriteToLog("Failed to get extraction record possibly due to extraction record was deleted."); } catch (Exception ex) { - DPCommon.WriteToLog($"Failed to get extraction record. REASON: {ex}"); - } finally { - if (c is null) { + // DPCommon.WriteToLog($"Failed to get extraction record. REASON: {ex}"); + } + finally + { + if (c is null) + { connection?.Dispose(); cmd?.Dispose(); } @@ -243,14 +242,14 @@ private static string[] GetTables(SQLiteConnection? c, CancellationToken cancell /// The SQLiteCOnnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). /// A hashset containing successfully extracted archive file names. - private static HashSet? GetArchiveFileNameList(SQLiteConnection? c, CancellationToken t) + private HashSet? GetArchiveFileNameList(SQLiteConnection? c, CancellationToken t) { SQLiteConnection _connection = null; SQLiteCommand cmd = null; SQLiteDataReader reader = null; HashSet names = null; var getCmd = @"SELECT ""Archive Name"" FROM ExtractionRecords;"; - var constring = "Data Source = " + Path.GetFullPath(_expectedDatabasePath) + ";Read Only=True"; + var constring = "Data Source = " + System.IO.Path.GetFullPath(Path) + ";Read Only=True"; try { _connection = CreateAndOpenConnection(c, true); @@ -267,9 +266,12 @@ private static string[] GetTables(SQLiteConnection? c, CancellationToken cancell } catch (Exception ex) { - DPCommon.WriteToLog($"Failed to get archive file name list. REASON: {ex}"); - } finally { - if (c is null) { + // DPCommon.WriteToLog($"Failed to get archive file name list. REASON: {ex}"); + } + finally + { + if (c is null) + { _connection?.Dispose(); cmd?.Dispose(); reader?.DisposeAsync(); @@ -285,11 +287,11 @@ private static string[] GetTables(SQLiteConnection? c, CancellationToken cancell /// The SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). /// The last product record ID in the database. - private static uint GetLastProductID(SQLiteConnection? conn, CancellationToken t) + private uint GetLastProductID(SQLiteConnection? conn, CancellationToken t) { if (t.IsCancellationRequested) return 0; var c = "SELECT ID FROM ProductRecords ORDER BY ID DESC LIMIT 1;"; - SQLiteConnection connection = null; + SQLiteConnection? connection = null; SQLiteCommand cmd = null; try { @@ -299,7 +301,7 @@ private static uint GetLastProductID(SQLiteConnection? conn, CancellationToken t } catch (Exception ex) { - DPCommon.WriteToLog($"Failed to get last product ID. REASON: {ex}"); + // DPCommon.WriteToLog($"Failed to get last product ID. REASON: {ex}"); } return 0; } @@ -312,11 +314,11 @@ private static uint GetLastProductID(SQLiteConnection? conn, CancellationToken t /// The SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). /// A dataset containing all of the values from the table specified. May return null. - private static DataSet? GetAllValuesFromTable(string tableName, SQLiteConnection? c, + private DataSet? GetAllValuesFromTable(string tableName, SQLiteConnection? c, CancellationToken token) { DataSet dataset = null; - SQLiteConnection connection = null; + SQLiteConnection? connection = null; SQLiteCommand sqlCommand = null; if (token.IsCancellationRequested) return dataset; try @@ -324,7 +326,7 @@ private static uint GetLastProductID(SQLiteConnection? conn, CancellationToken t connection = CreateAndOpenConnection(c, true); var getCommand = $"SELECT * FROM {tableName}"; sqlCommand = new SQLiteCommand(getCommand, connection); - SQLiteDataAdapter adapter = new SQLiteDataAdapter(sqlCommand); + SQLiteDataAdapter adapter = new(sqlCommand); dataset = new DataSet(tableName); adapter.Fill(dataset); //ViewUpdated?.Invoke(dataset); @@ -343,7 +345,7 @@ private static uint GetLastProductID(SQLiteConnection? conn, CancellationToken t /// The SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). /// Whether the removal was a success (true) or not (false). - private static bool RemoveAllRecords(SQLiteConnection? c, CancellationToken t) + private bool RemoveAllRecords(SQLiteConnection? c, CancellationToken t) { if (t.IsCancellationRequested) return false; @@ -351,7 +353,7 @@ private static bool RemoveAllRecords(SQLiteConnection? c, CancellationToken t) // Faster way is to drop the table & re-make it. // TODO: Drop table and remake it. var deleteCommand = $"DELETE FROM ProductRecords; DELETE FROM ExtractionRecords;"; - SQLiteConnection connection = null; + SQLiteConnection? connection = null; SQLiteTransaction transaction = null; SQLiteCommand sqlCommand = null; try @@ -374,9 +376,10 @@ private static bool RemoveAllRecords(SQLiteConnection? c, CancellationToken t) TableUpdated?.Invoke("ProductRecords"); TableUpdated?.Invoke("ExtractionRecords"); } - } catch (Exception ex) + } + catch (Exception ex) { - DPCommon.WriteToLog($"Failed to delete records. REASON: {ex}"); + // DPCommon.WriteToLog($"Failed to delete records. REASON: {ex}"); transaction.Rollback(); return false; } @@ -384,15 +387,17 @@ private static bool RemoveAllRecords(SQLiteConnection? c, CancellationToken t) } catch (Exception ex) { - DPCommon.WriteToLog($"Failed to create and begin transaction. REASON: {ex}"); + // DPCommon.WriteToLog($"Failed to create and begin transaction. REASON: {ex}"); return false; - } finally { + } + finally + { if (c is null) { sqlCommand?.Dispose(); connection?.Dispose(); } - + } return true; @@ -406,16 +411,16 @@ private static bool RemoveAllRecords(SQLiteConnection? c, CancellationToken t) /// The SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). /// Whether the removal was a success (true) or not (false). - private static bool RemoveProductRecordsViaTag(string[] tags, SQLiteConnection? c, + private bool RemoveProductRecordsViaTag(string[] tags, SQLiteConnection? c, CancellationToken t) { if (t.IsCancellationRequested) return false; if (tags.Length == 0) return true; - string args = ConvertParamsToString(tags); - string idsCommand = $"SELECT \"Product Record ID\" FROM Tags WHERE Tag IN ({args})"; - string deleteCommand = $"DELETE FROM ProductRecords WHERE ID IN ({idsCommand});"; - SQLiteConnection connection = null; + var args = ConvertParamsToString(tags); + var idsCommand = $"SELECT \"Product Record ID\" FROM Tags WHERE Tag IN ({args})"; + var deleteCommand = $"DELETE FROM ProductRecords WHERE ID IN ({idsCommand});"; + SQLiteConnection? connection = null; SQLiteCommand sqlCommand = null; SQLiteTransaction transaction = null; try @@ -429,22 +434,26 @@ private static bool RemoveProductRecordsViaTag(string[] tags, SQLiteConnection? sqlCommand.ExecuteNonQuery(); transaction.Commit(); TableUpdated.Invoke("ProductRecords"); - } catch (Exception ex) + } + catch (Exception ex) { - DPCommon.WriteToLog($"Failed to delete from ProductRecords where ID IN {args}. REASON: {ex.Message}"); + // DPCommon.WriteToLog($"Failed to delete from ProductRecords where ID IN {args}. REASON: {ex.Message}"); transaction.Rollback(); return false; } } catch (Exception ex) { - DPCommon.WriteToLog($"Failed to create connection and transaction. REASON: {ex}"); + // DPCommon.WriteToLog($"Failed to create connection and transaction. REASON: {ex}"); return false; - } finally { - if (c is null) { + } + finally + { + if (c is null) + { connection?.Dispose(); sqlCommand?.Dispose(); - + } } @@ -459,18 +468,18 @@ private static bool RemoveProductRecordsViaTag(string[] tags, SQLiteConnection? /// The SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). /// Whether the removal was a success (true) or not (false). - private static bool RemoveValuesWithCondition(string tableName, Tuple[] conditions, + private bool RemoveValuesWithCondition(string tableName, Tuple[] conditions, bool or, SQLiteConnection? c, CancellationToken t) { // Build columns. - List args = new List(conditions.Length); + List args = new(conditions.Length); var vals = new object[conditions.Length]; - StringBuilder builder = new StringBuilder(250); + StringBuilder builder = new(250); builder.Append($"DELETE FROM {tableName}"); - + for (var i = 0; i < conditions.Length; i++) { - var tuple = conditions[i]; + Tuple tuple = conditions[i]; var column = tuple.Item1; var item = tuple.Item2; var arg = $"@A{i}"; @@ -485,7 +494,7 @@ private static bool RemoveValuesWithCondition(string tableName, TupleThe SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). /// Whether the removal was a success (true) or not (false). - private static bool RemoveAllFromTable(string tableName, SQLiteConnection? c, CancellationToken t) + private bool RemoveAllFromTable(string tableName, SQLiteConnection? c, CancellationToken t) { if (t.IsCancellationRequested) return false; - + var deleteCommand = $"DELETE FROM {tableName};"; // Faster way is to drop the table & re-make it. - SQLiteConnection connection = null; + SQLiteConnection? connection = null; SQLiteTransaction transaction = null; SQLiteCommand sqlCommand = null; try @@ -547,9 +560,10 @@ private static bool RemoveAllFromTable(string tableName, SQLiteConnection? c, Ca sqlCommand.ExecuteNonQuery(); transaction.Commit(); TableUpdated?.Invoke(tableName); - } catch (Exception ex) + } + catch (Exception ex) { - DPCommon.WriteToLog($"Failed delete all values for table: {tableName}. REASON: {ex.Message}"); + // DPCommon.WriteToLog($"Failed delete all values for table: {tableName}. REASON: {ex.Message}"); transaction.Rollback(); return false; } @@ -557,10 +571,13 @@ private static bool RemoveAllFromTable(string tableName, SQLiteConnection? c, Ca } catch (Exception ex) { - DPCommon.WriteToLog($"Failed to create connection and transaction. REASON: {ex}"); + // DPCommon.WriteToLog($"Failed to create connection and transaction. REASON: {ex}"); return false; - } finally { - if (c is null) { + } + finally + { + if (c is null) + { connection?.Dispose(); sqlCommand?.Dispose(); } @@ -574,7 +591,7 @@ private static bool RemoveAllFromTable(string tableName, SQLiteConnection? c, Ca /// The product record ID to remove tags associated with it. /// The SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). - private static bool RemoveTags(uint pid, SQLiteConnection? c, CancellationToken t) + private bool RemoveTags(uint pid, SQLiteConnection? c, CancellationToken t) { return RemoveValuesWithCondition("Tags", new Tuple[] { new Tuple("Product Record ID", pid) } @@ -590,44 +607,47 @@ private static bool RemoveTags(uint pid, SQLiteConnection? c, CancellationToken /// An array of tags to insert into the database. /// The SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). - private static void InsertTags(string[] tags, SQLiteConnection? conn, CancellationToken t) + private void InsertTags(string[] tags, SQLiteConnection? conn, CancellationToken t) { if (t.IsCancellationRequested) return; - SQLiteConnection connection = null; + SQLiteConnection? connection = null; try { connection = CreateAndOpenConnection(conn); if (connection == null) return; - uint pid = GetLastProductID(connection, t); + var pid = GetLastProductID(connection, t); if (pid == 0) { - DPCommon.WriteToLog("Product ID returned 0; no tags added."); + // DPCommon.WriteToLog("Product ID returned 0; no tags added."); return; } - List tagsStripped = new List(tags.Length); - foreach (string tag in tags) + List tagsStripped = new(tags.Length); + foreach (var tag in tags) { if (string.IsNullOrEmpty(tag)) continue; - string tagTrimmed = tag.Trim(); + var tagTrimmed = tag.Trim(); if (tagTrimmed.Length == 0) continue; tagsStripped.Add(tagTrimmed); } - object[][] vals = new object[tagsStripped.Count][]; + var vals = new object[tagsStripped.Count][]; for (var i = 0; i < tagsStripped.Count; i++) { vals[i] = new object[] { tagsStripped[i], pid }; } - InsertMultipleValuesToTable("Tags", new string[] { "Tag", "Product Record ID" }, + InsertMultipleValuesToTable("Tags", new string[] { "Tag", "Product Record ID" }, vals, connection, t); - } catch (Exception ex) + } + catch (Exception ex) + { + // DPCommon.WriteToLog($"Failed to insert tags. REASON: {ex}"); + } + finally { - DPCommon.WriteToLog($"Failed to insert tags. REASON: {ex}"); - } finally { if (conn is null) connection?.Dispose(); } @@ -639,25 +659,25 @@ private static void InsertTags(string[] tags, SQLiteConnection? conn, Cancellati /// The product record ID to use. /// The SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). - private static void InsertTags(string[] tags, uint pid, SQLiteConnection? conn, CancellationToken t) + private void InsertTags(string[] tags, uint pid, SQLiteConnection? conn, CancellationToken t) { if (t.IsCancellationRequested || pid == 0) return; - SQLiteConnection connection = null; + SQLiteConnection? connection = null; try { connection = CreateAndOpenConnection(conn); if (connection == null) return; - List tagsStripped = new List(tags.Length); - foreach (string tag in tags) + List tagsStripped = new(tags.Length); + foreach (var tag in tags) { if (string.IsNullOrEmpty(tag)) continue; - string tagTrimmed = tag.Trim(); + var tagTrimmed = tag.Trim(); if (tagTrimmed.Length == 0) continue; tagsStripped.Add(tagTrimmed); } - object[][] vals = new object[tagsStripped.Count][]; + var vals = new object[tagsStripped.Count][]; for (var i = 0; i < tagsStripped.Count; i++) { vals[i] = new object[] { tagsStripped[i], pid }; @@ -669,7 +689,7 @@ private static void InsertTags(string[] tags, uint pid, SQLiteConnection? conn, } catch (Exception ex) { - DPCommon.WriteToLog($"Failed to insert tags. REASON: {ex}"); + // DPCommon.WriteToLog($"Failed to insert tags. REASON: {ex}"); } finally { @@ -690,11 +710,11 @@ private static void InsertTags(string[] tags, uint pid, SQLiteConnection? conn, /// The SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). /// Whether the insertion was successful (true) or not (false). - private static bool InsertMultipleValuesToTable(string tableName, string[] columns, object[][] values, + private bool InsertMultipleValuesToTable(string tableName, string[] columns, object[][] values, SQLiteConnection? c, CancellationToken t) { if (t.IsCancellationRequested) return false; - SQLiteConnection connection = null; + SQLiteConnection? connection = null; SQLiteTransaction transaction = null; SQLiteCommand sqlCommand = null; try @@ -713,9 +733,9 @@ private static bool InsertMultipleValuesToTable(string tableName, string[] colum columns[i] = '"' + columns[i] + '"'; } var columnsToAdd = string.Join(',', columns); - StringBuilder builder = new StringBuilder((values.Length) * 20); - List args = new List(values.Length * 5); - int startNum = 0; + StringBuilder builder = new((values.Length) * 20); + List args = new(values.Length * 5); + var startNum = 0; for (var i = 0; i < values.Length; i++) { var str = "("; @@ -728,7 +748,7 @@ private static bool InsertMultipleValuesToTable(string tableName, string[] colum builder.AppendLine(str + ','); } builder.Remove(builder.Length - 3, 2); - object[] valsFlattened = new object[startNum]; + var valsFlattened = new object[startNum]; var nextOpen = 0; for (var i = 0; i < values.Length; i++) { @@ -747,15 +767,19 @@ private static bool InsertMultipleValuesToTable(string tableName, string[] colum } catch (Exception ex) { - DPCommon.WriteToLog($"Failed to insert values to {columnsToAdd}. REASON: {ex}"); + // DPCommon.WriteToLog($"Failed to insert values to {columnsToAdd}. REASON: {ex}"); transaction.Rollback(); return false; } - } catch (Exception ex) + } + catch (Exception ex) + { + // DPCommon.WriteToLog($"An unexpected error occurred while inserting multiple values to table. REASON: {ex}"); + } + finally { - DPCommon.WriteToLog($"An unexpected error occurred while inserting multiple values to table. REASON: {ex}"); - } finally { - if (c is null) { + if (c is null) + { connection?.Dispose(); sqlCommand?.Dispose(); } @@ -771,11 +795,11 @@ private static bool InsertMultipleValuesToTable(string tableName, string[] colum /// The SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). /// Whether the insertion was successful (true) or not (false). - private static bool InsertDefaultValuesToTable(string tableName, SQLiteConnection? c, + private bool InsertDefaultValuesToTable(string tableName, SQLiteConnection? c, CancellationToken t) { if (t.IsCancellationRequested) return false; - SQLiteConnection connection = null; + SQLiteConnection? connection = null; SQLiteTransaction transaction = null; SQLiteCommand sqlCommand = null; try @@ -793,16 +817,20 @@ private static bool InsertDefaultValuesToTable(string tableName, SQLiteConnectio } catch (Exception ex) { - DPCommon.WriteToLog($"Failed to insert default values to {tableName}. REASON: {ex}"); + // DPCommon.WriteToLog($"Failed to insert default values to {tableName}. REASON: {ex}"); transaction.Rollback(); return false; } - } catch (Exception ex) + } + catch (Exception ex) { - DPCommon.WriteToLog($"An unexpected error occurred while inserting default values. REASON: {ex}"); + // DPCommon.WriteToLog($"An unexpected error occurred while inserting default values. REASON: {ex}"); return false; - } finally { - if (c is null) { + } + finally + { + if (c is null) + { connection?.Dispose(); sqlCommand?.Dispose(); } @@ -824,11 +852,11 @@ private static bool InsertDefaultValuesToTable(string tableName, SQLiteConnectio /// The SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). /// Whether the insertion was successful (true) or not (false). - private static bool InsertValuesToTable(string tableName, string[] columns, object[] values, + private bool InsertValuesToTable(string tableName, string[] columns, object[] values, SQLiteConnection? c, CancellationToken t) { if (t.IsCancellationRequested) return false; - SQLiteConnection connection = null; + SQLiteConnection? connection = null; SQLiteTransaction transaction = null; SQLiteCommand sqlCommand = null; try @@ -862,17 +890,21 @@ private static bool InsertValuesToTable(string tableName, string[] columns, obje } catch (Exception ex) { - DPCommon.WriteToLog($"Failed to insert values to {columnsToAdd}. REASON: {ex}"); + // DPCommon.WriteToLog($"Failed to insert values to {columnsToAdd}. REASON: {ex}"); transaction.Rollback(); return false; } - } catch (Exception ex) + } + catch (Exception ex) { - DPCommon.WriteToLog($"Failed to insert values to table. REASON: {ex}"); + // DPCommon.WriteToLog($"Failed to insert values to table. REASON: {ex}"); return false; - } finally { - if (c is null) { + } + finally + { + if (c is null) + { connection?.Dispose(); sqlCommand?.Dispose(); } @@ -893,16 +925,16 @@ private static bool InsertValuesToTable(string tableName, string[] columns, obje /// The extraction record to insert. Cannot be null. /// The SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). - private static void InsertRecords(DPProductRecord pRecord, DPExtractionRecord eRecord, + private void InsertRecords(DPProductRecord? pRecord, DPExtractionRecord? eRecord, SQLiteConnection? c, CancellationToken t) { // Trigger will update the product record's extraction record ID to the newly created record. - string[] pColumns = new string[] { "Product Name", "Tags", "Author", "SKU", "Date Created", "Thumbnail Full Path", }; - string[] eColumns = new string[] { "Archive Name", "Files", "Folders", "Destination Path", "Errored Files", "Error Messages" }; + var pColumns = new string[] { "Product Name", "Tags", "Author", "SKU", "Date Created", "Thumbnail Full Path", }; + var eColumns = new string[] { "Archive Name", "Files", "Folders", "Destination Path", "Errored Files", "Error Messages" }; if (t.IsCancellationRequested || pRecord is null || eRecord is null) return; - SQLiteConnection connection = null; + SQLiteConnection? connection = null; pRecord.Deconstruct(out var productName, out var tags, out var author, out var sku, - out var time, out var thumbnailPath, out var __, out var _); + out DateTime time, out var thumbnailPath, out var __, out var _); eRecord.Deconstruct(out var archiveFileName, out var destPath, out var files, out var erroredFiles, out var erroredMessages, out var folders, out _); // We do not care about UID. @@ -917,8 +949,8 @@ private static void InsertRecords(DPProductRecord pRecord, DPExtractionRecord eR author = author?.Length > 30 ? author.Substring(0, 30) : author; sku = sku?.Length > 10 ? sku.Substring(0, 10) : sku; - object[] pObjs = new object[] { productName, JoinString(", ", 70, tags), author, sku, time.ToFileTimeUtc(), thumbnailPath }; - object[] eObjs = new object[] { archiveFileName, JoinString(", ", 384, files), + var pObjs = new object[] { productName, JoinString(", ", 70, tags), author, sku, time.ToFileTimeUtc(), thumbnailPath }; + var eObjs = new object[] { archiveFileName, JoinString(", ", 384, files), JoinString(", ", 384, folders), destPath, JoinString(", ", 384, erroredFiles), JoinString(", ", 65536, erroredMessages) }; @@ -933,19 +965,23 @@ private static void InsertRecords(DPProductRecord pRecord, DPExtractionRecord eR if (success) { // Create a new extraction record to contain the PID and it's EID. - var newE = new DPExtractionRecord(archiveFileName, destPath, files, erroredFiles, erroredMessages, folders, lastID); + DPExtractionRecord newE = new(archiveFileName, destPath, files, erroredFiles, erroredMessages, folders, lastID); ExtractionRecordAdded?.Invoke(newE); } } // Create new product record to update ID. - var newP = new DPProductRecord(productName, tags, author, sku, time, thumbnailPath, lastID, lastID); - ProductRecordAdded?.Invoke(newP, lastID); + DPProductRecord newP = new(productName, tags, author, sku, time, thumbnailPath, lastID, lastID); + ProductRecordAdded?.Invoke(newP); } } - catch (Exception ex) { - DPCommon.WriteToLog($"An unexpected error occurred while attempting to insert records. REASON: {ex}"); - } finally { - if (c is null) { + catch (Exception ex) + { + // DPCommon.WriteToLog($"An unexpected error occurred while attempting to insert records. REASON: {ex}"); + } + finally + { + if (c is null) + { connection?.Dispose(); } } @@ -964,13 +1000,13 @@ private static void InsertRecords(DPProductRecord pRecord, DPExtractionRecord eR /// The SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). /// Whether the insertion was successful (true) or not (false). - private static bool UpdateValues(string tableName, string[] columns, object[] newValues, + private bool UpdateValues(string tableName, string[] columns, object[] newValues, int id, SQLiteConnection? c, CancellationToken t) { if (t.IsCancellationRequested) return false; - SQLiteConnection connection = null; - SQLiteTransaction transaction = null; - SQLiteCommand sqlCommand = null; + SQLiteConnection? connection = null; + SQLiteTransaction transaction = null!; + SQLiteCommand sqlCommand = null!; try { connection = CreateAndOpenConnection(c); @@ -994,17 +1030,21 @@ private static bool UpdateValues(string tableName, string[] columns, object[] ne } catch (Exception ex) { - DPCommon.WriteToLog($"Failed to update {tableName}.{string.Join(", ",columns)} REASON: {ex}"); + // DPCommon.WriteToLog($"Failed to update {tableName}.{string.Join(", ",columns)} REASON: {ex}"); transaction.Rollback(); return false; } - } catch (Exception ex) + } + catch (Exception ex) { - DPCommon.WriteToLog($"An unexpected error occurred while attempting to update values. REASON: {ex}"); + // DPCommon.WriteToLog($"An unexpected error occurred while attempting to update values. REASON: {ex}"); return false; - } finally { - if (c is null) { + } + finally + { + if (c is null) + { connection?.Dispose(); sqlCommand?.Dispose(); } @@ -1019,21 +1059,21 @@ private static bool UpdateValues(string tableName, string[] columns, object[] ne /// The SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). /// Whether the insertion was successful (true) or not (false). - private static bool UpdateProductRecord(uint pid, DPProductRecord newRecord, SQLiteConnection? c, CancellationToken t) + private bool UpdateProductRecord(uint pid, DPProductRecord newRecord, SQLiteConnection? c, CancellationToken t) { - string[] pColumns = new string[] { "Product Name", "Tags", "Author", "SKU", "Date Created", "Thumbnail Full Path", "Extraction Record ID"}; - if (t.IsCancellationRequested || newRecord == null || newRecord == DPProductRecord.NULL_RECORD || pid < 0) + var pColumns = new string[] { "Product Name", "Tags", "Author", "SKU", "Date Created", "Thumbnail Full Path", "Extraction Record ID" }; + if (t.IsCancellationRequested || newRecord == null || newRecord == DPProductRecord.NULL_RECORD || pid < 0) return false; newRecord.Deconstruct(out var productName, out var tags, out var author, out var sku, - out var time, out var thumbnailPath, out var eid, out var _); + out DateTime time, out var thumbnailPath, out var eid, out var _); // Shorten strings if applicable. productName = productName?.Length > 70 ? productName.Substring(0, 70) : productName; author = author?.Length > 30 ? author.Substring(0, 30) : author; sku = sku?.Length > 10 ? sku.Substring(0, 10) : sku; - object[] pObjs = new object[] { productName, JoinString(", ", 70, tags), author, sku, time.ToFileTimeUtc(), thumbnailPath, eid }; - SQLiteConnection connection = null; + var pObjs = new object[] { productName, JoinString(", ", 70, tags), author, sku, time.ToFileTimeUtc(), thumbnailPath, eid }; + SQLiteConnection? connection = null; SQLiteTransaction transaction = null; SQLiteCommand sqlCommand = null; try @@ -1042,7 +1082,7 @@ private static bool UpdateProductRecord(uint pid, DPProductRecord newRecord, SQL if (connection == null) return false; transaction = connection.BeginTransaction(); - var updateCommand = new StringBuilder(250); + StringBuilder updateCommand = new(250); updateCommand.Append("UPDATE ProductRecords SET "); for (var i = 0; i < 7; i++) { @@ -1067,7 +1107,7 @@ private static bool UpdateProductRecord(uint pid, DPProductRecord newRecord, SQL } catch (Exception ex) { - DPCommon.WriteToLog($"Failed to update {newRecord.ProductName} entry (ProductRecord). REASON: {ex}"); + // DPCommon.WriteToLog($"Failed to update {newRecord.ProductName} entry (ProductRecord). REASON: {ex}"); transaction.Rollback(); return false; } @@ -1075,7 +1115,7 @@ private static bool UpdateProductRecord(uint pid, DPProductRecord newRecord, SQL } catch (Exception ex) { - DPCommon.WriteToLog($"An unexpected error occurred while attempting to update product record {newRecord.ProductName}. REASON: {ex}"); + // DPCommon.WriteToLog($"An unexpected error occurred while attempting to update product record {newRecord.ProductName}. REASON: {ex}"); return false; } finally @@ -1098,17 +1138,17 @@ private static bool UpdateProductRecord(uint pid, DPProductRecord newRecord, SQL /// The SQLiteConnection to use, if any. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). /// Whether the insertion was successful (true) or not (false). - private static bool UpdateExtractionRecord(uint eid, DPExtractionRecord newRecord, SQLiteConnection? c, CancellationToken t) + private bool UpdateExtractionRecord(uint eid, DPExtractionRecord newRecord, SQLiteConnection? c, CancellationToken t) { - string[] eColumns = new string[] { "Archive Name", "Files", "Folders", "Destination Path", "Errored Files", "Error Messages", "Product Record ID" }; + var eColumns = new string[] { "Archive Name", "Files", "Folders", "Destination Path", "Errored Files", "Error Messages", "Product Record ID" }; if (t.IsCancellationRequested || newRecord == null || newRecord == DPExtractionRecord.NULL_RECORD || eid < 0) return false; newRecord.Deconstruct(out var archiveFileName, out var destPath, out var files, out var erroredFiles, out var erroredMessages, out var folders, out var newPID); - object[] eObjs = new object[] { archiveFileName, JoinString(", ", 384, files), + var eObjs = new object[] { archiveFileName, JoinString(", ", 384, files), JoinString(", ", 384, folders), destPath, JoinString(", ", 384, erroredFiles), JoinString(", ", 65536, erroredMessages), newPID }; - SQLiteConnection connection = null; + SQLiteConnection? connection = null; SQLiteTransaction transaction = null; SQLiteCommand sqlCommand = null; try @@ -1117,7 +1157,7 @@ private static bool UpdateExtractionRecord(uint eid, DPExtractionRecord newRecor if (connection == null) return false; transaction = connection.BeginTransaction(); - var updateCommand = new StringBuilder(250); + StringBuilder updateCommand = new(250); updateCommand.Append("UPDATE ExtractionRecords SET "); for (var i = 0; i < 7; i++) { @@ -1137,7 +1177,7 @@ private static bool UpdateExtractionRecord(uint eid, DPExtractionRecord newRecor } catch (Exception ex) { - DPCommon.WriteToLog($"Failed to update {newRecord.ArchiveFileName} entry (ExtractionRecord). REASON: {ex}"); + // DPCommon.WriteToLog($"Failed to update {newRecord.ArchiveFileName} entry (ExtractionRecord). REASON: {ex}"); transaction.Rollback(); return false; } @@ -1145,7 +1185,7 @@ private static bool UpdateExtractionRecord(uint eid, DPExtractionRecord newRecor } catch (Exception ex) { - DPCommon.WriteToLog($"An unexpected error occurred while attempting to update product record {newRecord.ArchiveFileName}. REASON: {ex}"); + // DPCommon.WriteToLog($"An unexpected error occurred while attempting to update product record {newRecord.ArchiveFileName}. REASON: {ex}"); return false; } finally @@ -1168,21 +1208,21 @@ private static bool UpdateExtractionRecord(uint eid, DPExtractionRecord newRecor /// journal and merge it into the database file. Additionally, it attempts to delete the journal files /// as well. /// - private static void TruncateJournal() + private void TruncateJournal() { var pragmaCheckpoint = "PRAGMA wal_checkpoint(TRUNCATE);"; try { - using var connection = CreateAndOpenConnection(null, false); + using SQLiteConnection? connection = CreateAndOpenConnection(null, false); if (connection == null) return; - using var cmd = new SQLiteCommand(pragmaCheckpoint, connection); + using SQLiteCommand cmd = new(pragmaCheckpoint, connection); cmd.ExecuteNonQuery(); } catch (Exception ex) { return; } // We don't want to delete if it failed. // Now check if -wal and -shm are available. - var shmFile = Path.GetFullPath(_expectedDatabasePath + "-shm"); - var walFile = Path.GetFullPath(_expectedDatabasePath + "-wal"); + var shmFile = System.IO.Path.GetFullPath(Path + "-shm"); + var walFile = System.IO.Path.GetFullPath(Path + "-wal"); // This is required for the SQLiteConnection to truly release the handle on // the database file. @@ -1209,12 +1249,12 @@ private static void TruncateJournal() /// The maximum string size of each value. Default is 256. /// The values to join. /// The values combined into a string seperated by the sepertor or null if values is null. - private static string? JoinString(string seperator, int maxSize = 256, params string[] values) + private string? JoinString(string seperator, int maxSize = 256, params string[] values) { if (values == null || values.Length == 0) return null; - StringBuilder builder = new StringBuilder(512); - for (int i = 0; i < values.Length; i++) + StringBuilder builder = new(512); + for (var i = 0; i < values.Length; i++) { var s = values[i]; if (string.IsNullOrWhiteSpace(s)) continue; @@ -1233,11 +1273,11 @@ private static void TruncateJournal() /// A referenced string of a query to add parameter placeholders. May not be null. /// The amount of parameters to create. /// An array of parameters generated. - private static string[] CreateParams(ref string str, int length) + private string[] CreateParams(ref string str, int length) { - int maxDigits = (int)Math.Floor(Math.Log10(length)) + 1; - StringBuilder sb = new StringBuilder((maxDigits + 4) * length); - string[] args = new string[length]; + var maxDigits = (int)Math.Floor(Math.Log10(length)) + 1; + StringBuilder sb = new((maxDigits + 4) * length); + var args = new string[length]; for (var i = 0; i < length; i++) { var rawArg = "@A" + i; @@ -1257,11 +1297,11 @@ private static string[] CreateParams(ref string str, int length) /// A referenced string of a query to add parameter placeholders. May not be null. /// The amount of parameters to create. For example, for updating two columns, the length should be 2, not 4. /// An array of parameters generated. - private static string[] CreateAssignmentParams(ref string str, int length) + private string[] CreateAssignmentParams(ref string str, int length) { - int maxDigits = (int)Math.Floor(Math.Log10(length)) + 1; - StringBuilder sb = new StringBuilder((maxDigits + 8) * length); - string[] args = new string[length * 2]; + var maxDigits = (int)Math.Floor(Math.Log10(length)) + 1; + StringBuilder sb = new((maxDigits + 8) * length); + var args = new string[length * 2]; for (var i = 0; i < length * 2; i += 2) { var rawArg = "@A" + i + " = @A" + (i + 1); @@ -1284,11 +1324,11 @@ private static string[] CreateAssignmentParams(ref string str, int length) /// The amount of parameters to create. /// An array of parameters generated. - private static string[] CreateParams(ref string str, int length, ref int start) + private string[] CreateParams(ref string str, int length, ref int start) { - int maxDigits = (int)Math.Floor(Math.Log10(length + start)) + 1; - StringBuilder sb = new StringBuilder((maxDigits + 4) * length); - string[] args = new string[length]; + var maxDigits = (int)Math.Floor(Math.Log10(length + start)) + 1; + StringBuilder sb = new((maxDigits + 4) * length); + var args = new string[length]; for (var i = 0; i < length; i++, start++) { var rawArg = "@A" + start; @@ -1308,7 +1348,7 @@ private static string[] CreateParams(ref string str, int length, ref int start) /// The command to add parameters into. Cannot be null. /// The argument placeholders to fill. Cannot be null. /// The values to replace placeholders with. Cannot be null. - private static void FillParamsToConnection(SQLiteCommand command, IReadOnlyList cArgs, params object[] values) + private void FillParamsToConnection(SQLiteCommand command, IReadOnlyList cArgs, params object[] values) { for (var i = 0; i < cArgs.Count; i++) { @@ -1329,12 +1369,12 @@ private static void FillParamsToConnection(SQLiteCommand command, IReadOnlyList< /// The values to replace placeholders with on the left side of the equal sign. Cannot be null. /// The values to replace placeholders with on the right side of the equal sign. Cannot be null. /// - private static void FillAssignmentParamsToConnection(SQLiteCommand command, IReadOnlyList cArgs, object[] leftVals, object[] rightVals) + private void FillAssignmentParamsToConnection(SQLiteCommand command, IReadOnlyList cArgs, object[] leftVals, object[] rightVals) { if (leftVals.Length != rightVals.Length) { - DPCommon.WriteToLog("FillAssignmentParamsToConnection() did not fill parameters due to unequal lengths in values."); - DPCommon.WriteToLog($"leftVals Length: {leftVals.Length} | rightVals Length: {rightVals.Length} "); + // DPCommon.WriteToLog("FillAssignmentParamsToConnection() did not fill parameters due to unequal lengths in values."); + // DPCommon.WriteToLog($"leftVals Length: {leftVals.Length} | rightVals Length: {rightVals.Length} "); } for (int i = 0, j = 0; i < cArgs.Count; i += 2, j++) { @@ -1350,14 +1390,14 @@ private static void FillAssignmentParamsToConnection(SQLiteCommand command, IRea /// Arguments to wrap quotes over. /// A string ready to use for a command. - private static string ConvertParamsToString(params object[] args) + private string ConvertParamsToString(params object[] args) { if (args.Length == 0) return string.Empty; - string[] sArgs = new string[args.Length]; + var sArgs = new string[args.Length]; for (var i = 0; i < args.Length; i++) { var arg = args[i]; - var type = arg.GetType(); + Type type = arg.GetType(); if (type == typeof(string)) sArgs[i] = '"' + (string)arg + '"'; else if (type == typeof(char)) sArgs[i] = '"' + (string)arg + '"'; else sArgs[i] = Convert.ToString(arg); diff --git a/src/DAZ_Installer.Database/DPDatabase.Public.cs b/src/DAZ_Installer.Database/DPDatabase.Public.cs new file mode 100644 index 0000000..d3c57c5 --- /dev/null +++ b/src/DAZ_Installer.Database/DPDatabase.Public.cs @@ -0,0 +1,188 @@ +using System.Data; +using System.Data.SQLite; + +namespace DAZ_Installer.Database +{ + public partial class DPDatabase : IDPDatabase + { + // This section is set up as an interface for other classes. You should use these methods + // to get data. These methods can callback if a callback is specified and emit an event. + // If you want to listen through an event, pass a constant caller id. + // Example: a constant caller ID for DPLibrary = 3. + #region Public methods + // TO DO: Improve. This can be so much more efficient. + // I lack the brain capacity to do this at the moment. + + public async Task SearchQ(string searchQuery, DPSortMethod sortMethod = DPSortMethod.None, + uint callerID = 0, Action? callback = null) + { + // We only want to do searches on one thread. Calling priority task manager ensures + // we only do searches on one thread. + _priorityTaskManager.Stop(); + DPProductRecord[] results = Array.Empty(); + await _priorityTaskManager.AddToQueue((t) => + { + results = DoSearchS(searchQuery, sortMethod, null, t); + callback?.Invoke(results); + SearchUpdated?.Invoke(results, callerID); + }); + return results; + } + + public async Task RegexSearchQ(string regex, DPSortMethod sortMethod = DPSortMethod.None, + uint callerID = 0, Action callback = null) + { + _priorityTaskManager.Stop(); + DPProductRecord[] results = Array.Empty(); + + await _priorityTaskManager.AddToQueue((t) => + { + results = DoRegexSearchS(regex, sortMethod, null, t); + callback?.Invoke(results); + SearchUpdated?.Invoke(results, callerID); + }); + + return results; + } + + public async Task GetProductRecordsQ(DPSortMethod sortMethod, uint page = 1, uint limit = 0, + uint callerID = 0, Action callback = null) + { + _priorityTaskManager.Stop(); + DPProductRecord[] results = Array.Empty(); + await _priorityTaskManager.AddToQueue((t) => + { + results = DoLibraryQuery(page, limit, sortMethod, null, t); + callback?.Invoke(results); + MainQueryCompleted?.Invoke(callerID); + }); + return results; + } + + + public void StopMainDatabaseOperations() => _mainTaskManager.Stop(); + + + public void StopAllDatabaseOperations() + { + _mainTaskManager.Stop(); + _priorityTaskManager.Stop(); + } + + #endregion + #region Queryable methods + public Task RefreshDatabaseQ(bool forceRefresh = false) + { + if (!forceRefresh) return _mainTaskManager.AddToQueue(RefreshDatabase); + _mainTaskManager.Stop(); + _priorityTaskManager.Stop(); + Initalized = false; + Initialize(); + return Task.CompletedTask; + } + + public async Task ViewTableQ(string tableName, uint callerID = 0, Action? callback = null) + { + DataSet? result = null; + await _mainTaskManager.AddToQueue((t) => + { + result = GetAllValuesFromTable(tableName, null, t); + callback?.Invoke(result); + ViewUpdated?.Invoke(result, callerID); + }); + return result; + } + + public Task AddNewRecordEntry(DPProductRecord pRecord) => _mainTaskManager.AddToQueue(InsertRecords, pRecord, null as DPExtractionRecord, null as SQLiteConnection); + + public Task AddNewRecordEntry(DPProductRecord pRecord, DPExtractionRecord eRecord) => _mainTaskManager.AddToQueue(InsertRecords, pRecord, eRecord, null as SQLiteConnection); + + public Task InsertNewRowQ(string tableName, object[] values, string[] columns) => _mainTaskManager.AddToQueue(InsertValuesToTable, tableName, columns, values, null as SQLiteConnection); + + public Task RemoveRowQ(string tableName, int id) + { + var arg = new Tuple[1] { new Tuple("ID", id) }; + return _mainTaskManager.AddToQueue(RemoveValuesWithCondition, tableName, arg, false, null as SQLiteConnection); + } + + public Task RemoveProductRecord(DPProductRecord record, Action? callback = null) + { + return _mainTaskManager.AddToQueue((t) => + { + var arg = new Tuple[1] { new Tuple("ID", Convert.ToInt32(record.ID)) }; + var success = RemoveValuesWithCondition("ProductRecords", arg, false, null, t); + if (success) + { + callback?.Invoke(record.ID); + ProductRecordRemoved?.Invoke(record.ID); + ExtractionRecordRemoved?.Invoke(record.EID); + } + }); + } + + public Task ClearTableQ(string tableName) => _mainTaskManager.AddToQueue(RemoveAllFromTable, tableName, null as SQLiteConnection); + + public Task UpdateValuesQ(string tableName, object[] values, string[] columns, int id) => _mainTaskManager.AddToQueue(UpdateValues, tableName, columns, values, id, null as SQLiteConnection); + + public Task UpdateRecordQ(uint id, DPProductRecord newProductRecord, DPExtractionRecord newExtractionRecord, Action? callback = null) + { + return _mainTaskManager.AddToQueue(t => + { + var success = UpdateProductRecord(id, newProductRecord, null, t); + if (!success) return; + success = UpdateExtractionRecord(id, newExtractionRecord, null, t); + if (!success) return; + callback?.Invoke(newProductRecord.ID); + ProductRecordModified?.Invoke(newProductRecord, id); + ExtractionRecordModified?.Invoke(newExtractionRecord, id); + }); + } + + public Task RemoveProductRecordsViaTagsQ(string[] tags) => + _mainTaskManager.AddToQueue(RemoveProductRecordsViaTag, tags, null as SQLiteConnection); + + public Task RemoveProductRecordsQ(Tuple condition) + { + var t = new Tuple[] { condition }; + return _mainTaskManager.AddToQueue(RemoveValuesWithCondition, "ProductRecords", t, false, null as SQLiteConnection); + } + + public Task RemoveProductRecordsQ(Tuple[] conditions) => _mainTaskManager.AddToQueue(RemoveValuesWithCondition, "ProductRecords", conditions, false, null as SQLiteConnection); + + public Task RemoveRowWithConditionQ(string tableName, Tuple condition) + { + var t = new Tuple[] { condition }; + return _mainTaskManager.AddToQueue(RemoveValuesWithCondition, tableName, t, false, null as SQLiteConnection); + } + + public Task RemoveRowWithConditionsQ(string tableName, Tuple[] conditions) => _mainTaskManager.AddToQueue(RemoveValuesWithCondition, tableName, conditions, false, null as SQLiteConnection); + + public Task RemoveAllRecordsQ() => _mainTaskManager.AddToQueue(RemoveAllRecords, null as SQLiteConnection); + + public Task RemoveTagsQ(uint pid) => _mainTaskManager.AddToQueue(RemoveTags, pid, null as SQLiteConnection); + + public async Task GetExtractionRecordQ(uint eid, uint callerID = 0, Action? callback = null) + { + DPExtractionRecord? result = null; + await _priorityTaskManager.AddToQueue((t) => + { + result = GetExtractionRecord(eid, null, t); + callback?.Invoke(result); + RecordQueryCompleted?.Invoke(result, callerID); + }); + return result; + } + + public async Task?> GetInstalledArchiveNamesQ(Action>? callback = null) + { + HashSet? result = null; + await _priorityTaskManager.AddToQueue((t) => + { + result = GetArchiveFileNameList(null, t); + callback?.Invoke(result); + }); + return result; + } + #endregion + } +} diff --git a/src/DP/DPDatabase.QueryProcessing.cs b/src/DAZ_Installer.Database/DPDatabase.QueryProcessing.cs similarity index 59% rename from src/DP/DPDatabase.QueryProcessing.cs rename to src/DAZ_Installer.Database/DPDatabase.QueryProcessing.cs index 0559786..70d56a8 100644 --- a/src/DP/DPDatabase.QueryProcessing.cs +++ b/src/DAZ_Installer.Database/DPDatabase.QueryProcessing.cs @@ -1,15 +1,13 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE -using System; +using DAZ_Installer.Database.External; using System.Data; -using System.Text; -using System.Threading; using System.Data.SQLite; -using DAZ_Installer.External; +using System.Text; -namespace DAZ_Installer.DP +namespace DAZ_Installer.Database { - public static partial class DPDatabase + public partial class DPDatabase { /// /// Generates SQL command based on search query and returns a sorted list of products. @@ -17,23 +15,24 @@ public static partial class DPDatabase /// The raw search query from the user. /// The sort method to perform. /// - private static DPProductRecord[] DoSearchS(string searchQuery, DPSortMethod method, + private DPProductRecord[] DoSearchS(string searchQuery, DPSortMethod method, SQLiteConnection c, CancellationToken t) { DPProductRecord[] results = Array.Empty(); try { - using var connection = CreateAndOpenConnection(c, true); + using SQLiteConnection? connection = CreateAndOpenConnection(c, true); if (connection == null) return results; - using var command = new SQLiteCommand(connection); + using SQLiteCommand command = new(connection); SetupSQLSearchLikeQuery(searchQuery, true, method, command); // SetupSQLSearchQuery(searchQuery, method, command); results = SearchProductRecordsViaTagsS(command, t); UpdateProductRecordCount(connection, t); UpdateExtractionRecordCount(connection, t); - } catch (Exception ex) + } + catch (Exception ex) { - DPCommon.WriteToLog($"An error occurred doing a regular search. REASON: {ex}"); + // DPCommon.WriteToLog($"An error occurred doing a regular search. REASON: {ex}"); } return results; } @@ -43,32 +42,31 @@ private static DPProductRecord[] DoSearchS(string searchQuery, DPSortMethod meth /// /// The regex to perform from the user. /// - private static DPProductRecord[] DoRegexSearchS(string regex, DPSortMethod method, + private DPProductRecord[] DoRegexSearchS(string regex, DPSortMethod method, SQLiteConnection c, CancellationToken t) { - var results = Array.Empty(); + DPProductRecord[] results = Array.Empty(); try { - using (var connection = CreateAndOpenConnection(c, true)) - { - if (connection == null) return results; - - var attribute = (SQLiteFunctionAttribute)typeof(SQLRegexFunction).GetCustomAttributes(typeof(SQLiteFunctionAttribute), true)[0]; - connection.BindFunction(attribute, new SQLRegexFunction()); - SpinWait.SpinUntil(() => connection.State != ConnectionState.Connecting - || connection.State != ConnectionState.Executing - || connection.State != ConnectionState.Fetching); - if (connection.State == ConnectionState.Broken) return results; - using var command = new SQLiteCommand(connection); - SetupSQLRegexQuery(regex, method, command); - results = SearchProductRecordsViaTagsS(command, t); - command.Dispose(); - UpdateProductRecordCount(connection, t); - UpdateExtractionRecordCount(connection, t); - } - } catch (Exception e) + using SQLiteConnection? connection = CreateAndOpenConnection(c, true); + if (connection == null) return results; + + var attribute = (SQLiteFunctionAttribute)typeof(SQLRegexFunction).GetCustomAttributes(typeof(SQLiteFunctionAttribute), true)[0]; + connection.BindFunction(attribute, new SQLRegexFunction()); + SpinWait.SpinUntil(() => connection.State != ConnectionState.Connecting + || connection.State != ConnectionState.Executing + || connection.State != ConnectionState.Fetching); + if (connection.State == ConnectionState.Broken) return results; + using SQLiteCommand command = new(connection); + SetupSQLRegexQuery(regex, method, command); + results = SearchProductRecordsViaTagsS(command, t); + command.Dispose(); + UpdateProductRecordCount(connection, t); + UpdateExtractionRecordCount(connection, t); + } + catch (Exception e) { - DPCommon.WriteToLog($"An error occurred with the regex search function. REASON: {e}"); + // DPCommon.WriteToLog($"An error occurred with the regex search function. REASON: {e}"); } return results; } @@ -77,24 +75,23 @@ private static DPProductRecord[] DoRegexSearchS(string regex, DPSortMethod metho /// /// The limit amount of results to return. /// The sorting method to apply to query results. - private static DPProductRecord[] DoLibraryQuery(uint page, uint limit, DPSortMethod method, + private DPProductRecord[] DoLibraryQuery(uint page, uint limit, DPSortMethod method, SQLiteConnection c, CancellationToken t) { - var results = Array.Empty(); + DPProductRecord[] results = Array.Empty(); try { - using (var _connection = CreateAndOpenConnection(c, true)) - { - if (_connection == null) return results; - using var command = new SQLiteCommand(_connection); - SetupSQLLibraryQuery(page, limit, method, command); - results = SearchProductRecordsViaTagsS(command, t); - UpdateProductRecordCount(_connection, t); - UpdateExtractionRecordCount(_connection, t); - } - } catch (Exception ex) + using SQLiteConnection? _connection = CreateAndOpenConnection(c, true); + if (_connection == null) return results; + using SQLiteCommand command = new(_connection); + SetupSQLLibraryQuery(page, limit, method, command); + results = SearchProductRecordsViaTagsS(command, t); + UpdateProductRecordCount(_connection, t); + UpdateExtractionRecordCount(_connection, t); + } + catch (Exception ex) { - DPCommon.WriteToLog($"An error occurred with the search function. {ex}"); + // DPCommon.WriteToLog($"An error occurred with the search function. {ex}"); } return results; } @@ -105,9 +102,9 @@ private static DPProductRecord[] DoLibraryQuery(uint page, uint limit, DPSortMet /// The regex to perform. Cannot be null. /// The sorting method to use for search results. Cannot be null. /// The command to set up the query for. Cannot be null. - private static void SetupSQLRegexQuery(string regex, DPSortMethod method, SQLiteCommand command) + private void SetupSQLRegexQuery(string regex, DPSortMethod method, SQLiteCommand command) { - string sqlQuery = @"SELECT DISTINCT * FROM ProductRecords WHERE ID IN (SELECT ""Product Record ID"" FROM Tags WHERE Tag REGEXP @A"; + var sqlQuery = @"SELECT DISTINCT * FROM ProductRecords WHERE ID IN (SELECT ""Product Record ID"" FROM Tags WHERE Tag REGEXP @A"; switch (method) { @@ -135,10 +132,10 @@ private static void SetupSQLRegexQuery(string regex, DPSortMethod method, SQLite /// The maximum number of items to return. /// The sorting method to use for search results. Cannot be null. /// The command to set up the query for. Cannot be null. - private static void SetupSQLLibraryQuery(uint page, uint limit, DPSortMethod method, SQLiteCommand command) + private void SetupSQLLibraryQuery(uint page, uint limit, DPSortMethod method, SQLiteCommand command) { - uint beginningRowID = (page - 1) * limit; - string sqlQuery = $"SELECT * FROM ProductRecords "; + var beginningRowID = (page - 1) * limit; + var sqlQuery = $"SELECT * FROM ProductRecords "; switch (method) { @@ -160,37 +157,28 @@ private static void SetupSQLLibraryQuery(uint page, uint limit, DPSortMethod met /// The user search query to process. /// The sorting method to use for search results. Cannot be null. /// The command to set up the query for. Cannot be null. - private static void SetupSQLSearchQuery(string userQuery, DPSortMethod method, SQLiteCommand command) + private void SetupSQLSearchQuery(string userQuery, DPSortMethod method, SQLiteCommand command) { - string[] tokens = userQuery.Split(' '); - string sqlQuery = @"SELECT DISTINCT * FROM ProductRecords WHERE ID IN (SELECT ""Product Record ID"" FROM Tags WHERE Tag IN ("; - StringBuilder sb = new StringBuilder(((int)Math.Floor(Math.Log10(tokens.Length)) + 1) * tokens.Length + (4 * tokens.Length)); - for (int i = 0; i < tokens.Length; i++) + var tokens = userQuery.Split(' '); + var sqlQuery = @"SELECT DISTINCT * FROM ProductRecords WHERE ID IN (SELECT ""Product Record ID"" FROM Tags WHERE Tag IN ("; + StringBuilder sb = new(((int)Math.Floor(Math.Log10(tokens.Length)) + 1) * tokens.Length + (4 * tokens.Length)); + for (var i = 0; i < tokens.Length; i++) { sb.Append(i == tokens.Length - 1 ? "@A" + i : "@A" + i + ", "); } sqlQuery += sb.ToString(); - switch (method) + sqlQuery += method switch { - case DPSortMethod.Alphabetical: - sqlQuery += @")) ORDER BY ""Product Name"" COLLATE NOCASE ASC;"; - break; - case DPSortMethod.Date: - sqlQuery += @")) ORDER BY ""Date Created"" ASC;"; - break; - case DPSortMethod.Relevance: - sqlQuery += @"GROUP BY ""Product Record ID"" ORDER BY COUNT(*) DESC));"; - break; - default: - sqlQuery += "));"; - break; - } - + DPSortMethod.Alphabetical => @")) ORDER BY ""Product Name"" COLLATE NOCASE ASC;", + DPSortMethod.Date => @")) ORDER BY ""Date Created"" ASC;", + DPSortMethod.Relevance => @"GROUP BY ""Product Record ID"" ORDER BY COUNT(*) DESC));", + _ => "));", + }; command.CommandText = sqlQuery; - for (int i = 0; i < tokens.Length; i++) + for (var i = 0; i < tokens.Length; i++) { command.Parameters.Add(new SQLiteParameter("@A" + i, tokens[i])); } @@ -207,13 +195,13 @@ private static void SetupSQLSearchQuery(string userQuery, DPSortMethod method, S /// Whether or not to use wildcards on both sides of search query. /// The sorting method to use for search results. Cannot be null. /// The command to set up the query for. Cannot be null. - private static void SetupSQLSearchLikeQuery(string userQuery, bool bothSides, DPSortMethod method, SQLiteCommand command) + private void SetupSQLSearchLikeQuery(string userQuery, bool bothSides, DPSortMethod method, SQLiteCommand command) { - string[] tokens = userQuery.Split(' '); - string sqlQuery = @"SELECT DISTINCT * FROM ProductRecords WHERE ID IN (SELECT ""Product Record ID"" FROM Tags WHERE Tag LIKE "; - StringBuilder sb = new StringBuilder(((int)Math.Floor(Math.Log10(tokens.Length)) + 1) * tokens.Length + (14 * tokens.Length)); - for (int i = 0; i < tokens.Length; i++) - { + var tokens = userQuery.Split(' '); + var sqlQuery = @"SELECT DISTINCT * FROM ProductRecords WHERE ID IN (SELECT ""Product Record ID"" FROM Tags WHERE Tag LIKE "; + StringBuilder sb = new(((int)Math.Floor(Math.Log10(tokens.Length)) + 1) * tokens.Length + (14 * tokens.Length)); + for (var i = 0; i < tokens.Length; i++) + { if (bothSides) sb.Append(i == tokens.Length - 1 ? "@A" + i : "@A" + i + " OR TAG LIKE "); @@ -223,7 +211,7 @@ private static void SetupSQLSearchLikeQuery(string userQuery, bool bothSides, DP } sqlQuery += sb.ToString(); - switch (method) + switch (method) { case DPSortMethod.Alphabetical: sqlQuery += @") ORDER BY ""Product Name"" COLLATE NOCASE ASC;"; @@ -241,7 +229,7 @@ private static void SetupSQLSearchLikeQuery(string userQuery, bool bothSides, DP command.CommandText = sqlQuery; - for (int i = 0; i < tokens.Length; i++) + for (var i = 0; i < tokens.Length; i++) { if (bothSides) command.Parameters.Add(new SQLiteParameter("@A" + i, '%' + tokens[i] + '%')); diff --git a/src/DP/DPDatabase.cs b/src/DAZ_Installer.Database/DPDatabase.cs similarity index 72% rename from src/DP/DPDatabase.cs rename to src/DAZ_Installer.Database/DPDatabase.cs index e57f38a..ba753ee 100644 --- a/src/DP/DPDatabase.cs +++ b/src/DAZ_Installer.Database/DPDatabase.cs @@ -1,21 +1,18 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE -using System; +using DAZ_Installer.Core; +using Serilog; using System.Data; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using System.Data.SQLite; -using System.IO; -namespace DAZ_Installer.DP +namespace DAZ_Installer.Database { /// /// This class will handle all database operations such as initializing the database, creating tables, rows, deleting, etc. /// Database will be run on a different thread aside from the main thread. /// - public static partial class DPDatabase + public partial class DPDatabase // SELECT * FROM ProductRecords WHERE ID IN(SELECT "Product Record ID" FROM TAGS WHERE Tag IN ("Run")) // Internal methods with suffix 'Q' are methods that can be queued to the TaskScheduler. // Some can be executed immediately, such as RefreshDatabase. @@ -34,93 +31,101 @@ public static partial class DPDatabase // TODO: Tool to use backup database. { // TODO : Hold last transcations. // Public - public static bool DatabaseExists { get; private set; } = false; - public static bool Initalized { get; private set; } = false; - public static string[] tableNames; + public ILogger Logger { get; set; } = Log.Logger; + public bool DatabaseExists { get; private set; } = false; + public bool Initalized { get; private set; } = false; + public string[] tableNames; - public static uint ProductRecordCount { get; private set; } = 0; - public static uint ExtractionRecordCount { get; private set; } = 0; - public static HashSet ArchiveFileNames { get; private set; } = new HashSet(); + public uint ProductRecordCount { get; private set; } = 0; + public uint ExtractionRecordCount { get; private set; } = 0; + public HashSet ArchiveFileNames { get; private set; } = new HashSet(); // Events /// /// This event is invoked whenever a Search function has completed searching whether any results were found or not. /// - public static event Action SearchUpdated; + public event Action? SearchUpdated; /// /// This event is invoked whenever the database schema, database connection status, or other database configurations have been changed. /// - public static event Action DatabaseUpdated; + public event Action? DatabaseUpdated; /// /// This event is invoked whenever a table has been updated; updated being having rows, columns removed, modified, or added. /// - public static event Action TableUpdated; + public event Action? TableUpdated; /// /// This event is invoked whenever a request to view the table of the database has been called and successfully finished the request. /// - public static event Action ViewUpdated; + public event Action? ViewUpdated; /// /// This event is currently not being used. /// - public static event Action LibraryQueryCompleted; + public event Action? LibraryQueryCompleted; /// /// This event is invoked whenever requesting for an extraction record has been successfully completed. /// - public static event Action RecordQueryCompleted; + public event Action? RecordQueryCompleted; /// /// This event is invoked whenever a library query has been completed regardless if it yields any product records or not. /// - public static event Action MainQueryCompleted; + public event Action? MainQueryCompleted; // Product Record events /// /// This event is invoked whenever a product record has been removed (aside from when the table has been cleared). /// - public static event Action ProductRecordRemoved; + public event Action? ProductRecordRemoved; /// /// This event is invoked whenever a product record has been modified. /// - public static event Action ProductRecordModified; + public event Action? ProductRecordModified; /// /// This event is invoked whenever a new product record has been added. /// - public static event Action ProductRecordAdded; + public event Action? ProductRecordAdded; /// /// This event is invoked whenever an extraction record has been removed (aside from when the table has been cleared). /// - public static event Action ExtractionRecordRemoved; + public event Action? ExtractionRecordRemoved; /// /// This event is invoked whenever a extraction record has been modified. /// - public static event Action ExtractionRecordModified; + public event Action? ExtractionRecordModified; /// /// This event is invoked whenever a new extraction record has been added. /// - public static event Action ExtractionRecordAdded; + public event Action? ExtractionRecordAdded; /// /// This event is invoked whenever all of the records have been removed from the database. /// - public static event Action RecordsCleared; + public event Action? RecordsCleared; + /// + /// The path of the database to use. Default is: %TEMP%\db.db. + /// + public string Path { get; init; } = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "db.db"); - private static string _expectedDatabasePath { get => Path.Join(DPSettings.databasePath, "db.db"); } + //private string _expectedDatabasePath => Path.Join(DPSettings.databasePath, "db.db"); // Main task manager... - private static DPTaskManager _mainTaskManager = new DPTaskManager(); + private DPTaskManager _mainTaskManager = new(); - private static DPTaskManager _priorityTaskManager = new DPTaskManager(); + private DPTaskManager _priorityTaskManager = new(); // Task state. private const byte DATABASE_VERSION = 2; - private static bool _initializing = false; - - // Cache :D - // TODO: Limit cache to 5. - // Might remove to keep low-memory profile. - private readonly static DPCache _columnsCache = new(); - - static DPDatabase() => Initialize(); + private bool _initializing = false; + ~DPDatabase() + { + StopAllDatabaseOperations(); + } + public DPDatabase(string path) + { + if (!path.EndsWith(".db")) throw new ArgumentException("Database path must end with .db"); + Path = path; + Initialize(); + } #region Private methods /// @@ -130,7 +135,7 @@ public static partial class DPDatabase /// to initalize. Otherwise, it will return true indicating it initalized successfully. /// /// True if initalization was successful, otherwise false. - private static bool Initialize() + private bool Initialize() { // If another thread is initalizing, wait for it to initalize or wait 10 secs max. try @@ -138,31 +143,32 @@ private static bool Initialize() if (_initializing) SpinWait.SpinUntil(() => _initializing = false, 10000); // Timeout throws an error, if we timed out intialized failed. - } catch { return false; } + } + catch { return false; } if (Initalized) return true; _initializing = true; try { // Check if database exists. - DatabaseExists = File.Exists(_expectedDatabasePath); + DatabaseExists = File.Exists(Path); // TODO: Check if const database version is higher than the one in the database. if (!DatabaseExists) { // Create the database. CreateDatabase(); // Update database info. - using var connection = CreateInitialConnection(); + using SQLiteConnection? connection = CreateInitialConnection(); if (connection == null) return false; InsertDefaultValuesToTable("DatabaseInfo", connection, CancellationToken.None); } DatabaseUpdated?.Invoke(); - DPGlobal.AppClosing += OnAppClose; Initalized = true; tableNames = GetTables(null, CancellationToken.None); - } catch (Exception ex) + } + catch (Exception ex) { - DPCommon.WriteToLog($"An error occurred while initializing. REASON: {ex}"); + // DPCommon.WriteToLog($"An error occurred while initializing. REASON: {ex}"); _initializing = false; return false; } @@ -175,7 +181,7 @@ private static bool Initialize() /// Determines if the connection should be a read-only /// connection or not. /// An SQLiteConnection if successfully created otherwise null. - private static SQLiteConnection? CreateConnection(bool readOnly = false) + private SQLiteConnection? CreateConnection(bool readOnly = false) { if (!Initalized) { @@ -184,16 +190,17 @@ private static bool Initialize() } try { - var connection = new SQLiteConnection(); - var builder = new SQLiteConnectionStringBuilder(); - builder.DataSource = Path.GetFullPath(_expectedDatabasePath); + SQLiteConnection connection = new(); + SQLiteConnectionStringBuilder builder = new(); + builder.DataSource = System.IO.Path.GetFullPath(Path); builder.Pooling = true; builder.ReadOnly = readOnly; connection.ConnectionString = builder.ConnectionString; - return connection; - } catch (Exception e) + return connection; + } + catch (Exception e) { - DPCommon.WriteToLog($"Failed to create connection. REASON: {e}"); + // DPCommon.WriteToLog($"Failed to create connection. REASON: {e}"); } return null; } @@ -201,20 +208,20 @@ private static bool Initialize() /// Creates and returns a connection with the connection string setup. Should only be used for the Initialization function. /// /// An SQLiteConnection if successfully created, otherwise null. - private static SQLiteConnection? CreateInitialConnection() + private SQLiteConnection? CreateInitialConnection() { try { - var connection = new SQLiteConnection(); - var builder = new SQLiteConnectionStringBuilder(); - builder.DataSource = Path.GetFullPath(_expectedDatabasePath); + SQLiteConnection connection = new(); + SQLiteConnectionStringBuilder builder = new(); + builder.DataSource = System.IO.Path.GetFullPath(Path); builder.Pooling = true; connection.ConnectionString = builder.ConnectionString; return connection; } catch (Exception e) { - DPCommon.WriteToLog($"Failed to create connection. REASON: {e}"); + // DPCommon.WriteToLog($"Failed to create connection. REASON: {e}"); } return null; } @@ -229,9 +236,9 @@ private static bool Initialize() /// The connection passed if it isn't null and was successfully opened. /// Otherwise, a new connection is passed if it was successfully opened. Otherwise, /// null is returned. - private static SQLiteConnection? CreateAndOpenConnection(SQLiteConnection? connection, bool readOnly = false) + private SQLiteConnection? CreateAndOpenConnection(SQLiteConnection? connection, bool readOnly = false) { - var c = connection ?? CreateConnection(readOnly); + SQLiteConnection? c = connection ?? CreateConnection(readOnly); var success = OpenConnection(c); return success ? c : null; } @@ -242,7 +249,7 @@ private static bool Initialize() /// /// The connection to open. /// True if the connection opened successfully, otherwise false. - private static bool OpenConnection(SQLiteConnection connection) + private bool OpenConnection(SQLiteConnection? connection) { if (connection == null) return false; if (connection.State != ConnectionState.Closed) return true; @@ -250,9 +257,10 @@ private static bool OpenConnection(SQLiteConnection connection) { connection.Open(); return true; - } catch (Exception ex) + } + catch (Exception ex) { - DPCommon.WriteToLog($"Failed to open connection. REASON: {ex}"); + // DPCommon.WriteToLog($"Failed to open connection. REASON: {ex}"); } return false; } @@ -260,13 +268,13 @@ private static bool OpenConnection(SQLiteConnection connection) /// /// Creates a new database file and sets it up for use. /// - private static void CreateDatabase() + private void CreateDatabase() { - - if (!Directory.Exists(_expectedDatabasePath)) - Directory.CreateDirectory(Path.GetDirectoryName(_expectedDatabasePath)); - SQLiteConnection.CreateFile(_expectedDatabasePath); + if (!Directory.Exists(Path)) + Directory.CreateDirectory(System.IO.Path.GetDirectoryName(Path)!); + + SQLiteConnection.CreateFile(Path); // Create tables, indexes, and triggers. CreateTables(); CreateIndexes(); @@ -279,9 +287,9 @@ private static void CreateDatabase() /// Does not check if they exist. May throw an error if the tables already exist. /// /// Whether creating tables was a success. - private static bool CreateTables() + private bool CreateTables() { - + const string createProductRecordsCommand = @" CREATE TABLE ""ProductRecords"" ( @@ -317,7 +325,7 @@ PRIMARY KEY(""ID"" AUTOINCREMENT) ""Result Product IDs"" TEXT, PRIMARY KEY(""Search String"") ); "; - string createDatabaseInfoCommand = $@" + var createDatabaseInfoCommand = $@" CREATE TABLE ""DatabaseInfo"" ( ""Version"" INTEGER NOT NULL DEFAULT {DATABASE_VERSION}, @@ -331,23 +339,23 @@ PRIMARY KEY(""Search String"") )"; try { - using (var connection = CreateInitialConnection()) - { - var success = OpenConnection(connection); - if (!success) return false; - var createCommand = new SQLiteCommand(createProductRecordsCommand, connection); - createCommand.ExecuteNonQuery(); - createCommand.CommandText = createExtractionRecordsCommand; - createCommand.ExecuteNonQuery(); - createCommand.CommandText = createCachedSearchCommand; - createCommand.ExecuteNonQuery(); - createCommand.CommandText = createDatabaseInfoCommand; - createCommand.ExecuteNonQuery(); - createCommand.CommandText = createTagsCommand; - createCommand.ExecuteNonQuery(); - } - } catch (Exception ex) { - DPCommon.WriteToLog($"An error occurred while attempting to create database. REASON: {ex}"); + using SQLiteConnection? connection = CreateInitialConnection(); + var success = OpenConnection(connection); + if (!success) return false; + SQLiteCommand createCommand = new(createProductRecordsCommand, connection); + createCommand.ExecuteNonQuery(); + createCommand.CommandText = createExtractionRecordsCommand; + createCommand.ExecuteNonQuery(); + createCommand.CommandText = createCachedSearchCommand; + createCommand.ExecuteNonQuery(); + createCommand.CommandText = createDatabaseInfoCommand; + createCommand.ExecuteNonQuery(); + createCommand.CommandText = createTagsCommand; + createCommand.ExecuteNonQuery(); + } + catch (Exception ex) + { + // DPCommon.WriteToLog($"An error occurred while attempting to create database. REASON: {ex}"); return false; } return true; @@ -357,7 +365,7 @@ PRIMARY KEY(""Search String"") /// Does not check if they exist. May throw an error if the tables already exist. /// /// Whether creating indexes was a success. - private static bool CreateIndexes() + private bool CreateIndexes() { const string createTagToPIDCommand = @" CREATE INDEX ""idx_TagToPID"" ON ""Tags"" ( @@ -385,25 +393,24 @@ private static bool CreateIndexes() try { - using (var connection = CreateInitialConnection()) + using (SQLiteConnection? connection = CreateInitialConnection()) { var success = OpenConnection(connection); if (!success) return false; - using (var cmdObj = new SQLiteCommand(createTagToPIDCommand, connection)) - { - cmdObj.ExecuteNonQuery(); - cmdObj.CommandText = createPIDtoTagCommand; - cmdObj.ExecuteNonQuery(); - cmdObj.CommandText = createProductNameToPIDCommand; - cmdObj.ExecuteNonQuery(); - cmdObj.CommandText = createDateCreatedToPIDCommand; - cmdObj.ExecuteNonQuery(); - } + using SQLiteCommand cmdObj = new(createTagToPIDCommand, connection); + cmdObj.ExecuteNonQuery(); + cmdObj.CommandText = createPIDtoTagCommand; + cmdObj.ExecuteNonQuery(); + cmdObj.CommandText = createProductNameToPIDCommand; + cmdObj.ExecuteNonQuery(); + cmdObj.CommandText = createDateCreatedToPIDCommand; + cmdObj.ExecuteNonQuery(); } DatabaseUpdated?.Invoke(); - } catch (Exception ex) + } + catch (Exception ex) { - DPCommon.WriteToLog($"An error occurred creating indexes. REASON: {ex}"); + // DPCommon.WriteToLog($"An error occurred creating indexes. REASON: {ex}"); return false; } return true; @@ -412,7 +419,8 @@ private static bool CreateIndexes() /// Adds the triggers required for application to properly execute into the database. /// /// Whether creating triggers was a success. - private static bool CreateTriggers() { + private bool CreateTriggers() + { const string deleteOnProductRemoveTriggerCommand = @"CREATE TRIGGER IF NOT EXISTS delete_on_product_removal AFTER DELETE ON ProductRecords FOR EACH ROW @@ -447,30 +455,28 @@ AFTER INSERT ON ProductRecords try { - using (var connection = CreateInitialConnection()) + using (SQLiteConnection? connection = CreateInitialConnection()) { var success = OpenConnection(connection); if (!success) return false; - using (var createCommand = new SQLiteCommand(deleteOnProductRemoveTriggerCommand, connection)) - { - createCommand.ExecuteNonQuery(); - createCommand.CommandText = deleteOnExtractionRemoveTriggerCommand; - createCommand.ExecuteNonQuery(); - createCommand.CommandText = updateOnExtractionInsertionTriggerCommand; - createCommand.ExecuteNonQuery(); - createCommand.CommandText = updateProductCountTriggerCommand; - createCommand.ExecuteNonQuery(); - } + using SQLiteCommand createCommand = new(deleteOnProductRemoveTriggerCommand, connection); + createCommand.ExecuteNonQuery(); + createCommand.CommandText = deleteOnExtractionRemoveTriggerCommand; + createCommand.ExecuteNonQuery(); + createCommand.CommandText = updateOnExtractionInsertionTriggerCommand; + createCommand.ExecuteNonQuery(); + createCommand.CommandText = updateProductCountTriggerCommand; + createCommand.ExecuteNonQuery(); } DatabaseUpdated?.Invoke(); } catch (Exception ex) { - DPCommon.WriteToLog($"An error occurred creating triggers. REASON: {ex}"); + // DPCommon.WriteToLog($"An error occurred creating triggers. REASON: {ex}"); return false; } return true; - + } /// /// Changes a few settings for how the database should act. This is required @@ -478,28 +484,27 @@ AFTER INSERT ON ProductRecords /// less journal sizes. /// /// Whether the execution was a success. - private static bool ExecutePragmas() + private bool ExecutePragmas() { const string pramaCommmands = @"PRAGMA journal_mode = WAL; PRAGMA wal_autocheckpoint=2; PRAGMA journal_size_limit=32768; PRAGMA page_size=512;"; - try { - using (var connection = CreateInitialConnection()) + try + { + using (SQLiteConnection? connection = CreateInitialConnection()) { var success = OpenConnection(connection); if (!success) return false; - using (var createCommand = new SQLiteCommand(pramaCommmands, connection)) - { - createCommand.ExecuteNonQuery(); - } + using SQLiteCommand createCommand = new(pramaCommmands, connection); + createCommand.ExecuteNonQuery(); } DatabaseUpdated?.Invoke(); } catch (Exception ex) { - DPCommon.WriteToLog($"Failed to execute pragmas. REASON: {ex}"); + // DPCommon.WriteToLog($"Failed to execute pragmas. REASON: {ex}"); return false; } return true; @@ -509,7 +514,8 @@ private static bool ExecutePragmas() /// Does not check if they exist. May throw an error if don't the tables already exist. /// /// Whether deleting triggers was a success. - private static bool DeleteTriggers() { + private bool DeleteTriggers() + { const string removeTriggersCommand = @"DROP TRIGGER IF EXISTS delete_on_extraction_removal; @@ -519,18 +525,18 @@ private static bool DeleteTriggers() { try { - using (var connection = CreateInitialConnection()) + using (SQLiteConnection? connection = CreateInitialConnection()) { var success = OpenConnection(connection); if (!success) return false; - using var deleteCommand = new SQLiteCommand(removeTriggersCommand, connection); + using SQLiteCommand deleteCommand = new(removeTriggersCommand, connection); deleteCommand.ExecuteNonQuery(); } DatabaseUpdated?.Invoke(); } catch (Exception ex) { - DPCommon.WriteToLog($"An error occurred removing triggers. REASON: {ex}"); + // DPCommon.WriteToLog($"An error occurred removing triggers. REASON: {ex}"); return false; } return true; @@ -543,7 +549,7 @@ private static bool DeleteTriggers() { /// The SQLiteConnection to use, if any. Recommended to use a connection, otherwise use DeleteTriggers() instead. /// Cancel token. Required, cannot be null. Use CancellationToken.None instead (though not recommended). /// Whether deleting triggers was a success. - private static bool TempDeleteTriggers(SQLiteConnection c, CancellationToken token) + private bool TempDeleteTriggers(SQLiteConnection c, CancellationToken token) { if (token.IsCancellationRequested) return false; const string removeTriggersCommand = @@ -551,7 +557,7 @@ private static bool TempDeleteTriggers(SQLiteConnection c, CancellationToken tok DROP TRIGGER IF EXISTS delete_on_product_removal; DROP TRIGGER IF EXISTS update_on_extraction_add; DROP TRIGGER IF EXISTS update_product_count;"; - SQLiteConnection connection = null; + SQLiteConnection? connection = null; SQLiteCommand deleteCommand = null; try { @@ -562,9 +568,10 @@ private static bool TempDeleteTriggers(SQLiteConnection c, CancellationToken tok } catch (Exception ex) { - DPCommon.WriteToLog($"An error occurred removing triggers. REASON: {ex}"); + // DPCommon.WriteToLog($"An error occurred removing triggers. REASON: {ex}"); return false; - } finally + } + finally { if (c == null) { @@ -582,7 +589,7 @@ private static bool TempDeleteTriggers(SQLiteConnection c, CancellationToken tok /// Cancel token. Required, cannot be null. Use CancellationToken.None instead and if you wish to restore triggers that /// cannot be cancelled. /// Whether restoring the triggers was a success. - private static bool RestoreTriggers(SQLiteConnection c, CancellationToken token) + private bool RestoreTriggers(SQLiteConnection c, CancellationToken token) { if (token.IsCancellationRequested) return false; const string deleteOnProductRemoveTriggerCommand = @@ -617,12 +624,12 @@ AFTER INSERT ON ProductRecords UPDATE DatabaseInfo SET ""Product Record Count"" = (SELECT COUNT(*) FROM ProductRecords); END"; - SQLiteConnection connection = null; + SQLiteConnection? connection = null; SQLiteCommand createCommand = null; try { connection = CreateAndOpenConnection(c); - createCommand = new SQLiteCommand(deleteOnProductRemoveTriggerCommand, connection); + createCommand = new SQLiteCommand(deleteOnProductRemoveTriggerCommand, connection); createCommand.ExecuteNonQuery(); createCommand.CommandText = deleteOnExtractionRemoveTriggerCommand; createCommand.ExecuteNonQuery(); @@ -634,9 +641,10 @@ AFTER INSERT ON ProductRecords } catch (Exception ex) { - DPCommon.WriteToLog($"An error occurred creating triggers. REASON: {ex}"); + // DPCommon.WriteToLog($"An error occurred creating triggers. REASON: {ex}"); return false; - } finally + } + finally { if (c == null) { @@ -649,43 +657,59 @@ AFTER INSERT ON ProductRecords } // TO DO: Refresh database code. - private static void RefreshDatabase(CancellationToken t) { + private void RefreshDatabase(CancellationToken t) + { if (t.IsCancellationRequested) return; - try { - var task = Task.Run(_columnsCache.Clear); + try + { _mainTaskManager.StopAndWait(); _priorityTaskManager.StopAndWait(); Initalized = false; Initialize(); - task.Wait(); - } catch (Exception e) { - DPCommon.WriteToLog($"An unexpected error occured while attempting to refresh the database. REASON: {e}"); } - + catch (Exception e) + { + // DPCommon.WriteToLog($"An unexpected error occured while attempting to refresh the database. REASON: {e}"); + } + } - - private static void BackupDatabase(CancellationToken t) { - return; + + private void BackupDatabase(CancellationToken t) + { + using SQLiteConnection? c = CreateAndOpenConnection(null, true); + using SQLiteConnection d = new(); + SQLiteConnectionStringBuilder builder = new(); + + var newFileName = System.IO.Path.GetFileNameWithoutExtension(Path) + "_backup.db"; + builder.DataSource = System.IO.Path.GetFullPath(System.IO.Path.Combine(System.IO.Path.GetDirectoryName(Path)!, newFileName)); + d.ConnectionString = builder.ConnectionString; + try + { + c.BackupDatabase(d, "main", "main", -1, null, 5000); + } + catch { } // TODO: Log this. + } - private static void RestoreDatabase(CancellationToken t) { + private void RestoreDatabase(CancellationToken t) + { return; } - private static void RebuildDatabase(CancellationToken t) + private void RebuildDatabase(CancellationToken t) { } // Prep for app closure. - private static void OnAppClose(object e) + private void OnAppClose(object e) { _mainTaskManager.Stop(); _priorityTaskManager.Stop(); TruncateJournal(); } - + #endregion } } diff --git a/src/DP/DPExtractionRecord.cs b/src/DAZ_Installer.Database/DPExtractionRecord.cs similarity index 82% rename from src/DP/DPExtractionRecord.cs rename to src/DAZ_Installer.Database/DPExtractionRecord.cs index b55a730..8220c9c 100644 --- a/src/DP/DPExtractionRecord.cs +++ b/src/DAZ_Installer.Database/DPExtractionRecord.cs @@ -1,18 +1,17 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE -using System; -namespace DAZ_Installer.DP +namespace DAZ_Installer.Database { /// /// A DPRecord is a record of fully extracted and moved files. Records should be accessed via the Library and after successful extractions. /// - - public record DPExtractionRecord(string ArchiveFileName, string DestinationPath, + + public record DPExtractionRecord(string ArchiveFileName, string DestinationPath, string[] Files, string[] ErroredFiles, string[] ErrorMessages, string[] Folders, uint PID) { - public static readonly DPExtractionRecord NULL_RECORD = new DPExtractionRecord(null, null, null, null, null, null, 0); + public static readonly DPExtractionRecord NULL_RECORD = new(null, null, null, null, null, null, 0); } } diff --git a/src/DP/DPProductRecord.cs b/src/DAZ_Installer.Database/DPProductRecord.cs similarity index 78% rename from src/DP/DPProductRecord.cs rename to src/DAZ_Installer.Database/DPProductRecord.cs index 5e2d393..e3bbd90 100644 --- a/src/DP/DPProductRecord.cs +++ b/src/DAZ_Installer.Database/DPProductRecord.cs @@ -1,12 +1,11 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE -using System; -namespace DAZ_Installer.DP +namespace DAZ_Installer.Database { public record DPProductRecord(string ProductName, string[] Tags, string Author, string SKU, DateTime Time, string ThumbnailPath, uint EID, uint ID) { - public static readonly DPProductRecord NULL_RECORD = new DPProductRecord(null, null, null, null, DateTime.MinValue, null, 0, 0); + public static readonly DPProductRecord NULL_RECORD = new(null, null, null, null, DateTime.MinValue, null, 0, 0); } } diff --git a/src/DP/DPSortMethod.cs b/src/DAZ_Installer.Database/DPSortMethod.cs similarity index 87% rename from src/DP/DPSortMethod.cs rename to src/DAZ_Installer.Database/DPSortMethod.cs index 2ea7339..2999a53 100644 --- a/src/DP/DPSortMethod.cs +++ b/src/DAZ_Installer.Database/DPSortMethod.cs @@ -1,7 +1,7 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE -namespace DAZ_Installer.DP +namespace DAZ_Installer.Database { public enum DPSortMethod { diff --git a/src/External/SQLRegexFunction.cs b/src/DAZ_Installer.Database/External/SQLRegexFunction.cs similarity index 62% rename from src/External/SQLRegexFunction.cs rename to src/DAZ_Installer.Database/External/SQLRegexFunction.cs index 1c5ad18..fc60135 100644 --- a/src/External/SQLRegexFunction.cs +++ b/src/DAZ_Installer.Database/External/SQLRegexFunction.cs @@ -1,17 +1,13 @@ -using System; -using System.Data.SQLite; +using System.Data.SQLite; using System.Text.RegularExpressions; -namespace DAZ_Installer.External +namespace DAZ_Installer.Database.External { // Found from https://stackoverflow.com/questions/24229785/sqlite-net-sqlitefunction-not-working-in-linq-to-sql/26155359#26155359 // taken from http://sqlite.phxsoftware.com/forums/p/348/1457.aspx#1457 [SQLiteFunction(Name = "REGEXP", Arguments = 2, FuncType = FunctionType.Scalar)] internal class SQLRegexFunction : SQLiteFunction { - public override object Invoke(object[] args) - { - return Regex.IsMatch(Convert.ToString(args[1]), Convert.ToString(args[0])); - } + public override object Invoke(object[] args) => Regex.IsMatch(Convert.ToString(args[1]), Convert.ToString(args[0])); } } \ No newline at end of file diff --git a/src/DAZ_Installer.Database/IDPDatabase.cs b/src/DAZ_Installer.Database/IDPDatabase.cs new file mode 100644 index 0000000..3cc1d4c --- /dev/null +++ b/src/DAZ_Installer.Database/IDPDatabase.cs @@ -0,0 +1,191 @@ +using System; +using System.Data; +using System.Diagnostics; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DAZ_Installer.Database +{ + /// + /// The interface for the database. This is used for other classes to get data from the database. + /// All methods are executed asynchronously. + /// + public interface IDPDatabase + { + /// + /// Search asynchroniously does a database search based on a user search query (ex: "hello world"). Do not + /// use this function for regex expressions; Instead use RegexSearch. This function does not return + /// the search results but instead returns it to the callback and/or an event. If you wish to retrieve + /// the values through an event, use a caller ID to identify the event is for that particular class at that + /// time. + /// + /// A user search query. + /// The sorting method to apply for results. + /// A caller id for classifying event invocations. + /// The function to return values to. + /// The results of the search query. + Task SearchQ(string searchQuery, DPSortMethod sortMethod = DPSortMethod.None, uint callerID = 0, Action? callback = null); + /// + /// RegexSearch asynchroniously does a database search based on a user regex pattern (ex: "[^abc]"). + /// This function does not return the search results but instead returns it to the callback and/or an event. + /// If you wish to retrieve the values through an event, use a caller ID to identify the event is for that particular + /// class at that time. + /// + /// A regex expression. + /// The sorting method to apply for results. + /// A caller id for classifying event invocations. + /// The function to return values to. + /// The results of the search query. + Task RegexSearchQ(string regex, DPSortMethod sortMethod = DPSortMethod.None, uint callerID = 0, Action? callback = null); + /// + /// Gets product records on the page specified by . The page is determined by the + /// limit of . This means that if there are 50 records, and the limit is 10, the max + /// amount of pages is 5. This function does not return any results but will return the results via the callback + /// and via MainQueryCompleted event. + /// + /// The sorting method to apply to the results. + /// The page to get product records from. + /// The max amount of product records per page; the max amount of records to receive. + /// A caller id for classifying event invocations. + /// The function to return values to. + /// The results of the search query. + Task GetProductRecordsQ(DPSortMethod sortMethod, uint page = 1, uint limit = 0, uint callerID = 0, Action? callback = null); + /// + /// Stops the pending chain of main queries such as insert, update, and delete queries. + /// + void StopMainDatabaseOperations(); + /// + /// Stops the pending chain of main queries such as insert, update, delete, and get queries. + /// This also stops pending search queries and "view" queries. + /// + void StopAllDatabaseOperations(); + /// + /// If `forceRefresh` is false, the refresh action will be queued. Otherwise, the action queue will be cleared and database will be refreshed immediately. + /// + /// Refreshes immediately if True, otherwise it is queued. + Task RefreshDatabaseQ(bool forceRefresh = false); + /// + /// Returns all the values from the table specified by the table via the + /// callback or via ViewUpdated event. This may return null. + /// + /// The table name to + /// A caller id for classifying event invocations. + /// The function to return the dataset to. + /// The table, if successfully fetched. Otherwise, null. + Task ViewTableQ(string tableName, uint callerID = 0, Action? callback = null); + /// + /// Adds a new product record to the database. DOES NOT WORK! + /// + /// The new product record to add. + [Obsolete("This function does not work. Use AddNewRecordEntry(DPProductRecord, DPExtractionRecord) instead.")] + Task AddNewRecordEntry(DPProductRecord pRecord); + /// + /// Adds a new product and extraction record to the database. + /// + /// The new product record to add. + /// The new extraction record to add. + Task AddNewRecordEntry(DPProductRecord pRecord, DPExtractionRecord eRecord); + /// + /// Inserts a new row to the table specified by . It requires the columns that + /// new values will be inserted into. Columns and values length must match. + /// For example, if you want to insert a new row to ProductRecors but only want to insert the name for now, + /// columns would be {"Name"} and values would be {"poppy stick"}. Columns do not have to match + /// the columns in the database, but should include the required, non-null columns. + /// + /// The table to insert values into. + /// The values to insert. + /// The corresponding columns to insert values to. + Task InsertNewRowQ(string tableName, object[] values, string[] columns); + /// + /// Removes a row by it's ID at the table specifed by . + /// + /// The table to insert values into. + /// The ID of the row to remove. + Task RemoveRowQ(string tableName, int id); + /// + /// Removes a product record from the database. This also removes the corresponding extraction record. + /// + /// The record to delete. + /// The function to callback when the product record was removed. + /// + Task RemoveProductRecord(DPProductRecord record, Action? callback = null); + /// + /// Removes all the values from the table. Triggers in the database are temporarly disabled for deleting. + /// + /// + Task ClearTableQ(string tableName); + + /// + /// Not fully implemented. Do not use. + /// + /// + /// + /// + Task UpdateValuesQ(string tableName, object[] values, string[] columns, int id); + /// + /// Updates a product record and extraction record. This is currently used for applying changes from the product + /// record form. + /// + /// + /// + /// + Task UpdateRecordQ(uint id, DPProductRecord newProductRecord, DPExtractionRecord newExtractionRecord, Action? callback = null); + /// + /// Removes all product records (and corresonding extraction records) from the database that contain a tag + /// specified in tags. Basically, for every record in product records, if the product record contains ANY + /// of the tags specified in , it is removed from the database. + /// + /// Product records' tags to specify for deletion. + Task RemoveProductRecordsViaTagsQ(string[] tags); + /// + /// Removes all product records that satisfy the condition specified by . + /// + /// The prerequisite for removing a row that must be met. + Task RemoveProductRecordsQ(Tuple condition); + /// + /// Removes all product records that satisfy the conditions specified by . + /// + /// The prerequisites for removing a row that must be met. + Task RemoveProductRecordsQ(Tuple[] conditions); + /// + /// Removes all rows that satisfy the condition specified by . + /// + /// The table to remove rows from. + /// The prerequisite for removing a row that must be met. + Task RemoveRowWithConditionQ(string tableName, Tuple condition); + /// + /// Removes all rows that satisfy the conditions specified by . + /// + /// The table to remove rows from. + /// The prerequisite for removing a row that must be met. + Task RemoveRowWithConditionsQ(string tableName, Tuple[] conditions); + /// + /// Removes all product and extraction records from the database. + /// + Task RemoveAllRecordsQ(); + /// + /// Removes all tags associated with the product ID specified by from the database. + /// + /// + Task RemoveTagsQ(uint pid); + /// + /// Gets the extraction records associated with the extraction record ID specified by + /// from the database. This function returns the records via the callback function or via the RecordQueryCompleted + /// event. This may return null if the record does not exist in the database or an internal error occurred. + /// + /// The extraction record ID to get. + /// A caller id for classifying event invocations. + /// The function to return values to. + /// The extraction record, if successfully fetched. Otherwise, null. + Task GetExtractionRecordQ(uint eid, uint callerID = 0, Action? callback = null); + /// + /// Updates the ArchiveFileNames variable and returns it via callback function. + /// It returns a unique set of installed archive names. + /// + /// The function to return values to. + /// The archive file names. + Task> GetInstalledArchiveNamesQ(Action>? callback = null); + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.DatabaseTests/DAZ_Installer.DatabaseTests.csproj b/src/DAZ_Installer.DatabaseTests/DAZ_Installer.DatabaseTests.csproj new file mode 100644 index 0000000..cbf35e2 --- /dev/null +++ b/src/DAZ_Installer.DatabaseTests/DAZ_Installer.DatabaseTests.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + diff --git a/src/DAZ_Installer.DatabaseTests/DPDatabaseTests.cs b/src/DAZ_Installer.DatabaseTests/DPDatabaseTests.cs new file mode 100644 index 0000000..a3ade22 --- /dev/null +++ b/src/DAZ_Installer.DatabaseTests/DPDatabaseTests.cs @@ -0,0 +1,305 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using DAZ_Installer.Database; +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Serilog; +using System.Data.SQLite; +using System.ComponentModel; +using System.Configuration; +using Microsoft.VisualBasic; +using System.Runtime.InteropServices; +using System.Data; + +namespace DAZ_Installer.Database.Tests +{ + [TestClass] + public class DPDatabaseTests + { + public static DPDatabase Database { get; set; } + public static string DatabasePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".db"); + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Information() + .CreateLogger(); + } + [ClassCleanup] + public static void ClassCleanup() + { + try + { + SQLiteConnection.Shutdown(true, false); + GC.Collect(); + GC.WaitForPendingFinalizers(); + File.Delete(DatabasePath); + } + catch (Exception ex) + { + Log.Error("Failed to clean up test database (Class Cleanup).", ex); + } + } + + [TestInitialize] + public void TestInitialize() + { + Database = new DPDatabase(DatabasePath); + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + + [TestCleanup] + public void TestCleanup() + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + try + { + File.Delete(DatabasePath); + } + catch (Exception ex) + { + Log.Error("Failed to delete test database.", ex); + } + } + + [TestMethod] + public void AddNewRecordEntryTest() + { + var precord = new DPProductRecord("Test Product", new[] { "Test" }, "TheRealSolly", "", DateTime.UtcNow, "abc.png", 1, 1); + var erecord = new DPExtractionRecord("TestProduct.rar", "A:/b.rar", Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), 0); + + Database.AddNewRecordEntry(precord, erecord).Wait(); + + Database.ProductRecordAdded += (record) => Assert.That.ProductRecordEqual(precord, record); + Database.ExtractionRecordAdded += (record) => Assert.That.ExtractionRecordEqual(erecord, record); + + var results = DPDatabaseTestHelpers.GetAllProductRecords(DatabasePath); + Assert.AreEqual(1, results.Count); + Assert.That.ProductRecordEqual(precord, results[0]); + var results2 = DPDatabaseTestHelpers.GetAllExtractionRecords(DatabasePath); + Assert.AreEqual(1, results2.Count); + Assert.That.ExtractionRecordEqual(erecord, results2[0]); + } + [TestMethod] + public void AddNewRecordEntryTest_Multithreaded() + { + List> inputs = new(25); + object lockObj = new(); + + int counter = -1; + var counterFunc = new Func(() => Interlocked.Increment(ref counter)); + var tasks = DPDatabaseTestHelpers.ExecuteTasksSequentially(25, () => + { + var i = (uint) counterFunc(); + var precord = new DPProductRecord(i.ToString(), new[] { "Test" }, "TheRealSolly", "", DateTime.UtcNow, "abc.png", i + 1, i + 1); + var erecord = new DPExtractionRecord("TestProduct.rar", "A:/b.rar", Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), i); + inputs.Add(new Tuple(precord, erecord)); + Database.AddNewRecordEntry(precord, erecord).Wait(); + }); + Task.WaitAll(tasks.ToArray()); + + var results = DPDatabaseTestHelpers.GetAllProductRecords(DatabasePath); + var results2 = DPDatabaseTestHelpers.GetAllExtractionRecords(DatabasePath); + + Assert.AreEqual(results.Count, 25); + Assert.AreEqual(results2.Count, 25); + for (int i = 0; i < 25; i++) + { + (DPProductRecord precord, DPExtractionRecord erecord) = inputs[i]; + Assert.That.ProductRecordEqual(precord, results[i]); + Assert.That.ExtractionRecordEqual(erecord, results2[i]); + } + + } + + [TestMethod] + public void ViewTableQTest() + { + using var c = DPDatabaseTestHelpers.CreateConnection(DatabasePath); + using var cmd = c.CreateCommand(); + cmd.CommandText = "CREATE TABLE TestTable (ID INTEGER PRIMARY KEY, Name TEXT)"; + cmd.ExecuteNonQuery(); + cmd.CommandText = "INSERT INTO TestTable (Name) VALUES ('Test')"; + cmd.ExecuteNonQuery(); + cmd.Dispose(); + bool callbackCalled = false, eventCalled = false; + var assertLogicFunc = new Action(r => + { + Assert.AreEqual(r.Tables[0].DataSet.DataSetName, "TestTable"); + Assert.AreEqual(r.Tables[0].Rows.Count, 1); + Assert.AreEqual(r.Tables[0].Rows[0]["Name"], "Test"); + }); + var assertFunc = new Action(r => + { + Assert.IsFalse(callbackCalled); + assertLogicFunc(r); + callbackCalled = true; + }); + Database.ViewUpdated += (r, id) => + { + Assert.IsFalse(eventCalled); + Assert.AreEqual((uint) 1, id); + assertLogicFunc(r); + eventCalled = true; + }; + + var result = Database.ViewTableQ("TestTable", 1, callback: assertFunc).Result; + + assertLogicFunc(result); + } + + [TestMethod] + public void InsertNewRowQTest() + { + using var c = DPDatabaseTestHelpers.CreateConnection(DatabasePath); + using var cmd = c.CreateCommand(); + cmd.CommandText = "CREATE TABLE TestTable (ID INTEGER PRIMARY KEY, Name TEXT, Placeholder TEXT)"; + cmd.ExecuteNonQuery(); + c.Dispose(); + Database.TableUpdated += t => Assert.AreEqual("TestTable", t); + + Database.InsertNewRowQ("TestTable", new object[] { "Test" }, new string[] { "Name" }).Wait(); + + using var c2 = DPDatabaseTestHelpers.CreateConnection(DatabasePath); + using var cmd2 = c2.CreateCommand(); + cmd2.CommandText = "SELECT * FROM TestTable"; + using var reader = cmd2.ExecuteReader(); + Assert.IsTrue(reader.Read()); + Assert.AreEqual(reader["Name"], "Test"); + } + [TestMethod] + public void InsertNewRowQTest_Multithreaded() + { + using var c = DPDatabaseTestHelpers.CreateConnection(DatabasePath); + using var cmd = c.CreateCommand(); + cmd.CommandText = "CREATE TABLE TestTable (ID INTEGER PRIMARY KEY, Name TEXT, Placeholder TEXT)"; + cmd.ExecuteNonQuery(); + c.Dispose(); + // Create a counter function that yields an integer and always increases by 1 (use interlocked or any sync method). + int counter = -1; + var counterFunc = new Func(() => Interlocked.Increment(ref counter)); + var tasks = DPDatabaseTestHelpers.ExecuteTasksSequentially(25, + () => Database.InsertNewRowQ("TestTable", new object[] { counterFunc() }, new string[] { "Name" }).Wait()); + + Task.WaitAll(tasks.ToArray()); + + using var c2 = DPDatabaseTestHelpers.CreateConnection(DatabasePath); + using var cmd2 = c2.CreateCommand(); + cmd2.CommandText = "SELECT * FROM TestTable"; + using var reader = cmd2.ExecuteReader(); + for (int i = 0; i < 25; i++) + { + Console.WriteLine(i); + Assert.IsTrue(reader.Read()); + Assert.IsTrue(int.Parse((string) reader["Name"]) == i); + } + } + + [TestMethod] + public void RemoveRowQTest() + { + using var c = DPDatabaseTestHelpers.CreateConnection(DatabasePath); + using var cmd = c.CreateCommand(); + cmd.CommandText = "CREATE TABLE TestTable (ID INTEGER PRIMARY KEY, Name TEXT, Placeholder TEXT)"; + cmd.ExecuteNonQuery(); + cmd.CommandText = "INSERT INTO TestTable (Name) VALUES ('Test')"; + cmd.ExecuteNonQuery(); + c.Dispose(); + + Database.RemoveRowQ("TestTable", 1).Wait(); + + using var c2 = DPDatabaseTestHelpers.CreateConnection(DatabasePath, true); + using var cmd2 = c2.CreateCommand(); + cmd2.CommandText = "SELECT * FROM TestTable"; + using var reader = cmd2.ExecuteReader(); + Assert.IsFalse(reader.Read()); + } + + [TestMethod] + public void ClearTableQTest() + { + using var c = DPDatabaseTestHelpers.CreateConnection(DatabasePath); + using var cmd = c.CreateCommand(); + cmd.CommandText = "CREATE TABLE TestTable (ID INTEGER PRIMARY KEY, Name TEXT, Placeholder TEXT)"; + cmd.ExecuteNonQuery(); + cmd.CommandText = "INSERT INTO TestTable (Name) VALUES ('Test')"; + cmd.ExecuteNonQuery(); + c.Dispose(); + + Database.ClearTableQ("TestTable").Wait(); + + using var c2 = DPDatabaseTestHelpers.CreateConnection(DatabasePath, true); + using var cmd2 = c2.CreateCommand(); + cmd2.CommandText = "SELECT * FROM TestTable"; + using var reader = cmd2.ExecuteReader(); + Assert.IsFalse(reader.Read()); + } + + [TestMethod] + public void UpdateValuesQTest() + { + // Not fully implemented yet. + Assert.Inconclusive(); + using var c = DPDatabaseTestHelpers.CreateConnection(DatabasePath); + using var cmd = c.CreateCommand(); + cmd.CommandText = "CREATE TABLE TestTable (ID INTEGER PRIMARY KEY, Name TEXT, Placeholder TEXT)"; + cmd.ExecuteNonQuery(); + cmd.CommandText = "INSERT INTO TestTable (Name) VALUES ('Test')"; + cmd.ExecuteNonQuery(); + c.Dispose(); + + Database.UpdateValuesQ("TestTable", new string[] { "Test2" }, new string[] { "Name" }, 1).Wait(); + + using var c2 = DPDatabaseTestHelpers.CreateConnection(DatabasePath, true); + using var cmd2 = c2.CreateCommand(); + cmd2.CommandText = "SELECT * FROM TestTable"; + using var reader = cmd2.ExecuteReader(); + Assert.IsTrue(reader.Read()); + Assert.AreEqual(reader["Name"], "Test2"); + } + + [TestMethod] + public void UpdateRecordQTest() + { + var dummyRecord = new DPProductRecord("Test Product", new[] { "Test" }, "TheRealSolly", "", DateTime.UtcNow, "abc.png", 1, 1); + var dummyERecord = new DPExtractionRecord("TestProduct.rar", "A:/b.rar", Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), 0); + Database.AddNewRecordEntry(dummyRecord, dummyERecord).Wait(); + dummyRecord = dummyRecord with { Author = "" }; + + Database.UpdateRecordQ(1, dummyRecord, dummyERecord).Wait(); + + var a = DPDatabaseTestHelpers.GetAllProductRecords(DatabasePath); + var b = DPDatabaseTestHelpers.GetAllExtractionRecords(DatabasePath); + Assert.That.ProductRecordEqual(dummyRecord, a[0]); + Assert.That.ExtractionRecordEqual(dummyERecord, b[0]); + } + + [TestMethod] + public void GetExtractionRecordQTest() + { + var precord = new DPProductRecord("Test Product", new[] { "Test" }, "TheRealSolly", "", DateTime.UtcNow, "abc.png", 1, 1); + var erecord = new DPExtractionRecord("TestProduct.rar", "A:/b.rar", Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), 0); + Database.AddNewRecordEntry(precord, erecord).Wait(); + + var callBack = new Action(r => Assert.That.ExtractionRecordEqual(erecord, r)); + Database.RecordQueryCompleted += (r, id) => + { + Assert.AreEqual((uint) 1, id); + Assert.That.ExtractionRecordEqual(erecord, r); + }; + + var r = Database.GetExtractionRecordQ(1, 1, callback: callBack).Result; + + Assert.That.ExtractionRecordEqual(erecord, r); + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.DatabaseTests/Helpers/DPDatabaseTestHelpers.cs b/src/DAZ_Installer.DatabaseTests/Helpers/DPDatabaseTestHelpers.cs new file mode 100644 index 0000000..64c6113 --- /dev/null +++ b/src/DAZ_Installer.DatabaseTests/Helpers/DPDatabaseTestHelpers.cs @@ -0,0 +1,162 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Serilog; +using System.Data.SQLite; +using System.ComponentModel; +using System.Configuration; +using Microsoft.VisualBasic; +using System.Runtime.InteropServices; + +namespace DAZ_Installer.Database.Tests +{ + internal static class DPDatabaseTestHelpers + { + internal static SQLiteConnection? CreateConnection(string path, bool readOnly = false) + { + try + { + SQLiteConnection connection = new(); + SQLiteConnectionStringBuilder builder = new(); + builder.DataSource = Path.GetFullPath(path); + builder.Pooling = true; + builder.ReadOnly = readOnly; + connection.ConnectionString = builder.ConnectionString; + return connection.OpenAndReturn(); + } + catch (Exception e) + { + Log.Error("Failed to create connection.", e); + } + return null; + } + + internal static List GetAllProductRecords(string path) + { + using var connection = CreateConnection(path, true); + using var command = connection.CreateCommand(); + command.CommandText = "SELECT * FROM ProductRecords"; + using var reader = command.ExecuteReader(); + List searchResults = new(4); + string productName, author, thumbnailPath, sku; + string[] tags; + DateTime dateCreated; + uint extractionID, pid; + while (reader.Read()) + { + // Construct product records + // NULL values return type DB.NULL. + productName = (string)reader["Product Name"]; + // TODO: Tags have returned null; investigate why. + var rawTags = reader["Tags"] as string ?? string.Empty; + tags = rawTags.Trim().Split(", "); + author = reader["Author"] as string; // May return NULL + thumbnailPath = reader["Thumbnail Full Path"] as string; // May return NULL + extractionID = Convert.ToUInt32(reader["Extraction Record ID"] is DBNull ? + 0 : reader["Extraction Record ID"]); + dateCreated = DateTime.FromFileTimeUtc((long)reader["Date Created"]); + sku = reader["SKU"] as string; // May return NULL + pid = Convert.ToUInt32(reader["ID"]); + searchResults.Add( + new DPProductRecord(productName, tags, author, sku, dateCreated, thumbnailPath, extractionID, pid)); + } + return searchResults; + } + + internal static List GetAllExtractionRecords(string path) + { + using var connection = CreateConnection(path, true); + using var command = connection.CreateCommand(); + command.CommandText = "SELECT * FROM ExtractionRecords"; + using var reader = command.ExecuteReader(); + List searchResults = new(4); + while (reader.Read()) + { + string[] files, folders, erroredFiles, errorMessages; + var archiveFileName = reader["Archive Name"] as string; + var filesStr = reader["Files"] as string; + var foldersStr = reader["Folders"] as string; + var destinationPath = reader["Destination Path"] as string; + var erroredFilesStr = reader["Errored Files"] as string; + var errorMessagesStr = reader["Error Messages"] as string; + var pid = Convert.ToUInt32(reader["Product Record ID"]); + + files = filesStr?.Split(", ") ?? Array.Empty(); + folders = foldersStr?.Split(", ") ?? Array.Empty(); + erroredFiles = erroredFilesStr?.Split(", ") ?? Array.Empty(); + errorMessages = errorMessagesStr?.Split(", ") ?? Array.Empty(); + searchResults.Add( + new DPExtractionRecord(archiveFileName, destinationPath, files, erroredFiles, errorMessages, folders, pid)); + } + return searchResults; + } + + /// + /// A custom assertion for comparing two s. + /// + /// The expected DPProductRecord. + /// The actual DPProductRecord. + /// Choose whether to assert ID and EIDs or not. + internal static void ProductRecordEqual(this Assert a, DPProductRecord expected, DPProductRecord actual, bool assertIDs = false) + { + Assert.AreEqual(expected.ProductName, actual.ProductName); + CollectionAssert.AreEquivalent(expected.Tags, actual.Tags); + Assert.AreEqual(expected.Author, actual.Author); + Assert.AreEqual(expected.ThumbnailPath, actual.ThumbnailPath); + Assert.AreEqual(expected.SKU, actual.SKU); + Assert.AreEqual(expected.Time, actual.Time); + if (!assertIDs) return; + Assert.AreEqual(expected.ID, actual.ID); + Assert.AreEqual(expected.EID, actual.EID); + } + + /// + /// A custom assertion for comparing two s. + /// + /// The expected . + /// The actual . + /// Choose whether to assert IDs or not. + internal static void ExtractionRecordEqual(this Assert a, DPExtractionRecord expected, DPExtractionRecord actual, bool assertIDs = false) + { + Assert.AreEqual(expected.ArchiveFileName, actual.ArchiveFileName); + Assert.AreEqual(expected.DestinationPath, actual.DestinationPath); + CollectionAssert.AreEquivalent(expected.Files, actual.Files); + CollectionAssert.AreEquivalent(expected.Folders, actual.Folders); + CollectionAssert.AreEquivalent(expected.ErroredFiles, actual.ErroredFiles); + CollectionAssert.AreEquivalent(expected.ErrorMessages, actual.ErrorMessages); + if (!assertIDs) return; + Assert.AreEqual(expected.PID, actual.PID); + } + + /// + /// Executes the task sequentially on the thread pool times executing each time. + /// + /// The amount of times to execute the action sequentially. + /// The action to perform. + /// A list of tasks. + internal static List ExecuteTasksSequentially(uint n, Action action) + { + List tasks = new((int) n); + Task? lastTask = null; + for (uint j = 0; j < n; j++) + { + uint i = j; + if (lastTask is not null) tasks.Add(lastTask = lastTask.ContinueWith(_ => + { + Console.WriteLine(i); + action(); + })); + else tasks.Add(lastTask = Task.Factory.StartNew(() => + { + Console.WriteLine(i); + action(); + })); + } + return tasks; + } + } +} diff --git a/src/DAZ_Installer.IO/Abstractions/AbstractFileSystem.cs b/src/DAZ_Installer.IO/Abstractions/AbstractFileSystem.cs new file mode 100644 index 0000000..dde77ca --- /dev/null +++ b/src/DAZ_Installer.IO/Abstractions/AbstractFileSystem.cs @@ -0,0 +1,60 @@ +namespace DAZ_Installer.IO +{ + /// + /// An abstract class representing a file system. It is a factory class as well, creating , , and objects. + /// + public abstract class AbstractFileSystem + { + /// + /// The file scope to use for all IO nodes. + /// + public virtual DPFileScopeSettings Scope { get; set; } = DPFileScopeSettings.None; + + /// + public abstract void DeleteFile(string path); + /// + public abstract void DeleteDirectory(string path, bool recursive = false); + /// The file or directory to check. + /// If true, forcefully treats the path as a directory and checks if it exists. + /// + public abstract bool Exists(string? path, bool treatAsDirectory = false); + /// + public abstract IEnumerable EnumerateDirectories(string path); + /// + public abstract IEnumerable EnumerateFiles(string path); + /// + public abstract IDPDriveInfo[] GetDrives(); + + public abstract IDPFileInfo CreateFileInfo(string path); + /// + /// Creates a new instance. + /// + /// The path of the file to assign to the file info. + /// The directory of the file, if any. must have it's FileSystem set to this object. + /// Thrown if is not null and it's FileSystem is not this object." + internal protected abstract IDPFileInfo CreateFileInfo(string path, IDPDirectoryInfo? directory = null); + /// + /// Creates a new instance. + /// + /// The path of the directory to assign to the directory info. + public abstract IDPDirectoryInfo CreateDirectoryInfo(string path); + /// + /// Creates a new instance. + /// + /// The path of the directory to assign to the directory info. + /// The directory of this directory, if any. must have it's FileSystem set to this object. + internal protected abstract IDPDirectoryInfo CreateDirectoryInfo(string path, IDPDirectoryInfo? parent); + /// + /// Creates a new instance. + /// + /// The path containing the drive name; it doesn't have to only have the drive name. + public abstract IDPDriveInfo CreateDriveInfo(string path); + + public AbstractFileSystem() { } + /// + /// Sets the to . + /// + /// The scope to set. + public AbstractFileSystem(DPFileScopeSettings scope) => Scope = scope; + } +} diff --git a/src/DAZ_Installer.IO/Abstractions/IContextFactory.cs b/src/DAZ_Installer.IO/Abstractions/IContextFactory.cs new file mode 100644 index 0000000..71716d1 --- /dev/null +++ b/src/DAZ_Installer.IO/Abstractions/IContextFactory.cs @@ -0,0 +1,14 @@ +namespace DAZ_Installer.IO +{ + public interface IContextFactory + { + /// + AbstractFileSystem CreateContext(); + /// + AbstractFileSystem CreateContext(DPFileScopeSettings scope); + /// + AbstractFileSystem CreateContext(AbstractFileSystem context); + /// + AbstractFileSystem CreateContext(DPFileScopeSettings scope, DriveInfo? info); + } +} diff --git a/src/DAZ_Installer.IO/Abstractions/IDPDirectoryInfo.cs b/src/DAZ_Installer.IO/Abstractions/IDPDirectoryInfo.cs new file mode 100644 index 0000000..c900669 --- /dev/null +++ b/src/DAZ_Installer.IO/Abstractions/IDPDirectoryInfo.cs @@ -0,0 +1,41 @@ +namespace DAZ_Installer.IO +{ + public interface IDPDirectoryInfo : IDPIONode + { + /// + /// The parent directory of this directory. + /// + public IDPDirectoryInfo? Parent { get; } + /// + /// Create the directory on disk and necessary subdirectories. + /// + public void Create(); + /// + /// Deletes the directory on disk. If there are files and subdirectories, this will fail and throw an error unless is true. + /// + /// Setting this to will delete files and subdirectories along with this directory. + public void Delete(bool recursive); + /// + /// Moves the directory and it's contents to . must exist on disk. + /// + /// The path to move the directory and it's contents to. + public void MoveTo(string path); + /// + /// Indicates whether this operation is allowed. + /// + public bool PreviewCreate(); + /// + /// Indicates whether this operation is allowed. + /// + public bool PreviewDelete(bool recursive); + /// + /// Indicates whether this operation is allowed. + /// + public bool PreviewMoveTo(string path); + /// + /// Attempts to create the directory and necessary subdirectories. If the path is not whitelisted, it will fail. + /// + /// Whether the operation was successful or not. + bool TryCreate(); + } +} diff --git a/src/DAZ_Installer.IO/Abstractions/IDPDriveInfo.cs b/src/DAZ_Installer.IO/Abstractions/IDPDriveInfo.cs new file mode 100644 index 0000000..7903a4b --- /dev/null +++ b/src/DAZ_Installer.IO/Abstractions/IDPDriveInfo.cs @@ -0,0 +1,8 @@ +namespace DAZ_Installer.IO +{ + public interface IDPDriveInfo + { + long AvailableFreeSpace { get; } + IDPDirectoryInfo RootDirectory { get; } + } +} diff --git a/src/DAZ_Installer.IO/Abstractions/IDPFileInfo.cs b/src/DAZ_Installer.IO/Abstractions/IDPFileInfo.cs new file mode 100644 index 0000000..ee68345 --- /dev/null +++ b/src/DAZ_Installer.IO/Abstractions/IDPFileInfo.cs @@ -0,0 +1,34 @@ +namespace DAZ_Installer.IO +{ + public interface IDPFileInfo : IDPIONode + { + IDPDirectoryInfo? Directory { get; } + Stream Create(); + Stream Open(FileMode mode, FileAccess access); + Stream OpenRead(); + Stream OpenWrite(); + void MoveTo(string path, bool overwrite); + IDPFileInfo CopyTo(string path, bool overwrite); + void Delete(); + bool PreviewCreate(); + bool PreviewDelete(); + bool PreviewOpen(FileMode mode, FileAccess access); + bool PreviewMoveTo(string path, bool overwrite); + bool PreviewCopyTo(string path, bool overwrite); + bool TryDelete(); + bool TryOpen(FileMode mode, FileAccess access, out Stream? stream); + bool TryOpenRead(out Stream? stream); + bool TryOpenWrite(out Stream? stream); + bool TryMoveTo(string path, bool overwrite); + bool TryCopyTo(string path, bool overwrite, out IDPFileInfo? info); + bool TryAndFixDelete(out Exception? ex); + bool TryAndFixOpen(FileMode mode, FileAccess access, out Stream? stream, out Exception? ex); + bool TryAndFixOpenRead(out Stream? stream, out Exception? ex); + bool TryAndFixOpenWrite(out Stream? stream, out Exception? ex); + + bool TryAndFixMoveTo(string path, bool overwrite, out Exception? ex); + bool TryAndFixCopyTo(string path, bool overwrite, out IDPFileInfo? info, out Exception? ex); + + + } +} diff --git a/src/DAZ_Installer.IO/Abstractions/IDPFileScopeSettings.cs b/src/DAZ_Installer.IO/Abstractions/IDPFileScopeSettings.cs new file mode 100644 index 0000000..6d168d3 --- /dev/null +++ b/src/DAZ_Installer.IO/Abstractions/IDPFileScopeSettings.cs @@ -0,0 +1,13 @@ + +namespace DAZ_Installer.IO +{ + public interface IDPFileScopeSettings + { + bool ExplicitFilePaths { get; } + bool ExplicitDirectoryPaths { get; } + bool NoEnforcement { get; } + bool ThrowOnPathTransversal { get; } + public bool IsDirectoryWhitelisted(string directoryPath); + public bool IsFilePathWhitelisted(string path); + } +} diff --git a/src/DAZ_Installer.IO/Abstractions/IDPIONode.cs b/src/DAZ_Installer.IO/Abstractions/IDPIONode.cs new file mode 100644 index 0000000..bdf710b --- /dev/null +++ b/src/DAZ_Installer.IO/Abstractions/IDPIONode.cs @@ -0,0 +1,34 @@ +namespace DAZ_Installer.IO +{ + /// + /// Represents an DP IO node that extends the real , or + /// classes with and . + /// + public interface IDPIONode + { + /// + /// The filename of the object. + /// + public string Name { get; } + /// + /// The full path of the object. + /// + public string Path { get; } + /// + /// Determines whether this exists on disk. + /// + public bool Exists { get; } + /// + /// Determines whether the Path is whitelisted. + /// + public bool Whitelisted { get; } + /// + /// The attributes of the object. + /// + FileAttributes Attributes { get; set; } + /// + /// The context to use for this object. + /// + public AbstractFileSystem FileSystem { get; } + } +} diff --git a/src/DAZ_Installer.IO/Abstractions/IDirectoryInfo.cs b/src/DAZ_Installer.IO/Abstractions/IDirectoryInfo.cs new file mode 100644 index 0000000..a269087 --- /dev/null +++ b/src/DAZ_Installer.IO/Abstractions/IDirectoryInfo.cs @@ -0,0 +1,22 @@ +namespace DAZ_Installer.IO +{ + public interface IDirectoryInfo + { + // Properties + string Name { get; } + string FullName { get; } + bool Exists { get; } + FileAttributes Attributes { get; set; } + IDirectoryInfo? Parent { get; } + + // Methods + void Create(); + void Delete(bool recursive); + void MoveTo(string path); + IEnumerable EnumerateDirectories(); + IEnumerable EnumerateDirectories(string pattern, EnumerationOptions options); + IEnumerable EnumerateFiles(); + IEnumerable EnumerateFiles(string pattern, EnumerationOptions options); + + } +} diff --git a/src/DAZ_Installer.IO/Abstractions/IFileInfo.cs b/src/DAZ_Installer.IO/Abstractions/IFileInfo.cs new file mode 100644 index 0000000..2cf8435 --- /dev/null +++ b/src/DAZ_Installer.IO/Abstractions/IFileInfo.cs @@ -0,0 +1,17 @@ +namespace DAZ_Installer.IO +{ + public interface IFileInfo + { + string Name { get; } + string FullName { get; } + bool Exists { get; } + IDirectoryInfo? Directory { get; } + string? DirectoryName { get; } + FileAttributes Attributes { get; set; } + Stream Create(); + Stream Open(FileMode mode, FileAccess access); + void Delete(); + void MoveTo(string path, bool overwrite); + IFileInfo CopyTo(string path, bool overwrite); + } +} diff --git a/src/DAZ_Installer.IO/DAZ_Installer.IO.csproj b/src/DAZ_Installer.IO/DAZ_Installer.IO.csproj new file mode 100644 index 0000000..132c02c --- /dev/null +++ b/src/DAZ_Installer.IO/DAZ_Installer.IO.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/src/DAZ_Installer.IO/DPDirectoryInfo.cs b/src/DAZ_Installer.IO/DPDirectoryInfo.cs new file mode 100644 index 0000000..40e3e14 --- /dev/null +++ b/src/DAZ_Installer.IO/DPDirectoryInfo.cs @@ -0,0 +1,287 @@ +using DAZ_Installer.IO.Wrappers; +using System.Diagnostics.CodeAnalysis; +using System.IO; + +namespace DAZ_Installer.IO +{ + /// + /// A safer directory info class to with security to ensure that modifications on the disk are permitted via . + /// + public class DPDirectoryInfo : DPIONodeBase, IDPDirectoryInfo + { + /// + public override string Name => directoryInfo.Name; + + /// + public override string Path => directoryInfo.FullName; + + /// + public override bool Exists => directoryInfo.Exists; + public override FileAttributes Attributes { get => directoryInfo.Attributes; set => directoryInfo.Attributes = value; } + + /// + /// Determines whether the directory is whitelisted. + /// + public override bool Whitelisted => whitelisted; + /// + /// The fs that is currently used for this directory. + /// + public IDPFileScopeSettings Scope => FileSystem.Scope; + public override AbstractFileSystem FileSystem + { + get => fileSystem; + internal set + { + ArgumentNullException.ThrowIfNull(value); + fileSystem = value; + Invalidate(); + } + } + /// + public IDPDirectoryInfo? Parent => parent ??= tryCreateDirectoryInfoParent(directoryInfo, fileSystem); + + protected IDirectoryInfo directoryInfo; + protected IDPDirectoryInfo? parent; + protected bool whitelisted; + private AbstractFileSystem fileSystem; + + internal DPDirectoryInfo(DirectoryInfo info, AbstractFileSystem fs) : this(info, fs, tryCreateDirectoryInfoParent(info, fs)) { } + internal DPDirectoryInfo(IDirectoryInfo info, AbstractFileSystem fs) : this(info, fs, tryCreateDirectoryInfoParent(info, fs)) { } + internal DPDirectoryInfo(string path, AbstractFileSystem fs) : this(new DirectoryInfo(path), fs) { } + internal DPDirectoryInfo(DirectoryInfo info, AbstractFileSystem fs, IDPDirectoryInfo? parent) : this(new DirectoryInfoWrapper(info), fs, parent) { } + /// + /// Constructor used for testing and internally. + /// + /// The directory info to use. + /// The fs settings to use. + /// The parent directory of this directory. + internal DPDirectoryInfo(IDirectoryInfo info, AbstractFileSystem fs, IDPDirectoryInfo? parent) + { + ArgumentNullException.ThrowIfNull(info); + ArgumentNullException.ThrowIfNull(fs); + directoryInfo = info; + fileSystem = fs; + whitelisted = fs.Scope.IsDirectoryWhitelisted(info.FullName); + this.parent = parent; + } + + /// + /// + public void Create() + { + // We don't necessarily care if subdirectories required are created as well. But, we can change this later. For now, it stays. + throwIfNotWhitelisted(); + directoryInfo.Create(); + } + + /// + /// + public void Delete(bool recursive) + { + throwIfNotWhitelisted(); + // If recursive is true and we are in strict directories mode, we need to check all of the subdirectories and see if they are whitelisted. + throwIfChildrenNotWhitelisted(); + directoryInfo.Delete(recursive); + } + /// + /// + public void MoveTo(string path) + { + // Check if we have permission to 'modify' the current path. + // Since we are going to (technically) be removing it, so we need to check if we have permission to do so. + throwIfNotWhitelisted(); + throwIfChildrenNotWhitelisted(); + + // Now, we are checking if we have permission to modify the new path. + throwIfNotWhitelisted(path); + throwIfChildrenNotWhitelisted(path); + directoryInfo.MoveTo(path); + } + public override string ToString() => "DPDirectoryInfo: " + Path; + + #region Preview methods + public bool PreviewCreate() => Whitelisted; + public bool PreviewDelete(bool recursive) + { + if (!recursive) return Whitelisted; + try + { + throwIfChildrenNotWhitelisted(); + } + catch { return false; } + return true; + } + public bool PreviewMoveTo(string path) + { + if (!Whitelisted && !Scope.IsDirectoryWhitelisted(path)) return false; + try + { + throwIfChildrenNotWhitelisted(); + throwIfChildrenNotWhitelisted(path); + } + catch { return false; } + return true; + } + #endregion + #region Try methods + public bool TryCreate() + { + if (!Whitelisted) return false; + try + { + Create(); + return true; + } + catch { return false; } + } + + public bool TryDelete(bool recursive) + { + if (!recursive && !Whitelisted) return false; + try + { + Delete(recursive); + return true; + } catch { return false; } + } + + public bool TryMoveTo(string path) + { + if (!Whitelisted && !Scope.IsDirectoryWhitelisted(path)) return false; + try + { + MoveTo(path); + return true; + } catch { return false; } + } + #endregion + + #region TryAndFix Methods + /// + /// Attempts to move the file, and if an exception is thrown (aside from ), it will attempt to fix the problem. + /// If it couldn't, it will be returned. + /// + /// Whether the operation successfully executed (whitelisted and no exception after attempted recovery). + public bool TryAndFixMoveTo(string path, out Exception? exception) + { + exception = null; + if (!Whitelisted || !directoryInfo.Exists) return false; + var targetInfo = FileSystem.CreateFileInfo(path); + if (targetInfo.Exists && (targetInfo.Attributes.HasFlag(FileAttributes.ReadOnly) || targetInfo.Attributes.HasFlag(FileAttributes.Hidden))) + { + try + { + targetInfo.Attributes = FileAttributes.Normal; + } + catch (Exception ex) + { + exception = ex; + return false; + } + } + if (attemptWithRecovery(() => MoveTo(path), this, out exception)) return true; + return false; + } + /// + /// Attempts to move the file, and if an exception is thrown (aside from ), it will attempt to fix the problem. + /// If it couldn't, it will be returned. + /// + /// Whether the operation successfully executed (whitelisted and no exception after attempted recovery). + public bool TryAndFixDelete(bool recursive, [NotNullWhen(false)] out Exception? exception) + { + exception = null; + if (!Whitelisted || !directoryInfo.Exists) return false; + if (attemptWithRecovery(() => Delete(recursive), this, out exception)) return true; + return false; + } + #endregion + #region Private methods + private static IDPDirectoryInfo? tryCreateDirectoryInfoParent(DirectoryInfo info, AbstractFileSystem fs) => info.Parent == null ? null : new DPDirectoryInfo(info.Parent, fs, null); + private static IDPDirectoryInfo? tryCreateDirectoryInfoParent(IDirectoryInfo info, AbstractFileSystem fs) => info.Parent == null ? null : new DPDirectoryInfo(info.Parent, fs, null); + private void throwIfNotWhitelisted() + { + if (!Whitelisted) throw new OutOfScopeException(Path); + } + + private void throwIfNotWhitelisted(string path) + { + if (!Scope.IsDirectoryWhitelisted(path)) throw new OutOfScopeException(path); + } + + // TODO: This needs to be cached. Maybe + private void throwIfChildrenNotWhitelisted() + { + var enumOptions = new EnumerationOptions() + { + IgnoreInaccessible = true, + RecurseSubdirectories = true, + }; + + // Only do this one if explicit directory is set. Otherwise, do explict file paths, which will do the same thing. + // This will prevent enumerating twice. + if (Scope.ExplicitDirectoryPaths && !Scope.ExplicitFilePaths) + foreach (var directory in directoryInfo.EnumerateDirectories("*", enumOptions)) + { + if (!Scope.IsDirectoryWhitelisted(directory.FullName)) + throw new OutOfScopeException(directory.FullName, "Subdirectory is not whitelisted"); + } + else if (Scope.ExplicitFilePaths) + foreach (var file in directoryInfo.EnumerateFiles("*", enumOptions)) + { + if (!Scope.IsFilePathWhitelisted(file.FullName)) + throw new OutOfScopeException(file.FullName, "File is not whitelisted"); + } + } + private void throwIfChildrenNotWhitelisted(string path) => new DPDirectoryInfo(path, fileSystem).throwIfChildrenNotWhitelisted(); + internal override void Invalidate() + { + whitelisted = Scope.IsFilePathWhitelisted(Path); + } + private static bool attemptWithRecovery(Action a, IDPDirectoryInfo info, [NotNullWhen(false)] out Exception? ex) + { + ex = null; + try + { + if (!info.Exists) return false; + a(); + return true; + } + // There are two possible reasons for this exception: + // 1) The directory is readonly or hidden. + // 2) A file in the directory is readonly or hidden. + // 3) 1 and 2 but applied to subdirectories recursively. + // For now, we will deal with 1. + catch (UnauthorizedAccessException e) + { + try + { + info.Attributes = FileAttributes.Normal; + } + catch (Exception e2) + { + ex = new AggregateException(e, e2); + return false; + } + // Try again. + try + { + a(); return true; + } + catch (Exception e2) + { + ex = e2; + return false; + } + } + catch (Exception e) + { + ex = e; + return false; + } + } + + #endregion + + + } +} diff --git a/src/DAZ_Installer.IO/DPDriveInfo.cs b/src/DAZ_Installer.IO/DPDriveInfo.cs new file mode 100644 index 0000000..c68ca1d --- /dev/null +++ b/src/DAZ_Installer.IO/DPDriveInfo.cs @@ -0,0 +1,17 @@ +namespace DAZ_Installer.IO +{ + public class DPDriveInfo : IDPDriveInfo + { + public long AvailableFreeSpace => driveInfo.AvailableFreeSpace; + public IDPDirectoryInfo RootDirectory => new DPDirectoryInfo(driveInfo.RootDirectory, fs); + private readonly DriveInfo driveInfo; + private readonly AbstractFileSystem fs; + + public DPDriveInfo(string path, AbstractFileSystem fs) => (driveInfo, this.fs) = (new DriveInfo(path), fs); + internal DPDriveInfo(DriveInfo driveInfo, AbstractFileSystem fs) + { + this.driveInfo = driveInfo; + this.fs = fs; + } + } +} diff --git a/src/DAZ_Installer.IO/DPFileInfo.cs b/src/DAZ_Installer.IO/DPFileInfo.cs new file mode 100644 index 0000000..4df6240 --- /dev/null +++ b/src/DAZ_Installer.IO/DPFileInfo.cs @@ -0,0 +1,306 @@ +using System.Diagnostics.CodeAnalysis; +using DAZ_Installer.IO.Wrappers; +namespace DAZ_Installer.IO +{ + public class DPFileInfo : DPIONodeBase, IDPFileInfo + { + public override AbstractFileSystem FileSystem + { + get => fileSystem; + internal set + { + ArgumentNullException.ThrowIfNull(value); + fileSystem = value; + Invalidate(); + } + } + + public IDPFileScopeSettings Scope => FileSystem.Scope; + public override FileAttributes Attributes { get => fileInfo.Attributes; set => fileInfo.Attributes = value; } + + public IDPDirectoryInfo? Directory => directory ??= tryCreateDirectoryInfo(fileInfo, FileSystem); + + public override string Name => fileInfo.Name; + public override string Path => fileInfo.FullName; + public override bool Exists => fileInfo.Exists; + public override bool Whitelisted => whitelisted; + protected IFileInfo fileInfo; + protected AbstractFileSystem fileSystem; + protected IDPDirectoryInfo? directory; + protected bool whitelisted; + + + internal DPFileInfo(FileInfo fileInfo, AbstractFileSystem fs) : this(fileInfo, fs, tryCreateDirectoryInfo(fileInfo, fs)) { } + internal DPFileInfo(IFileInfo fileInfo, AbstractFileSystem fs) : this(fileInfo, fs, tryCreateDirectoryInfo(fileInfo, fs)) { } + + internal DPFileInfo(string path, AbstractFileSystem fs) : this(new FileInfo(path), fs) { } + internal DPFileInfo(FileInfo info, AbstractFileSystem fs, IDPDirectoryInfo? directory) : + this(new FileInfoWrapper(info), fs, directory) { } + /// + /// Constructor used for testing. + /// + /// The file info to use. + /// The file system to use. + /// The parent directory of this file. + internal DPFileInfo(IFileInfo info, AbstractFileSystem fs, IDPDirectoryInfo? directory) + { + ArgumentNullException.ThrowIfNull(info); + ArgumentNullException.ThrowIfNull(fs); + fileSystem = fs; + whitelisted = Scope.IsFilePathWhitelisted(info.FullName); + fileInfo = info; + this.directory = directory; + } + + public IDPFileInfo CopyTo(string path, bool overwrite) + { + throwIfNotWhitelisted(path); + return new DPFileInfo(fileInfo.CopyTo(path, overwrite), FileSystem); + } + public Stream Create() { + throwIfNotWhitelisted(); + return fileInfo.Create(); + } + /// + public void MoveTo(string path, bool overwrite) + { + throwIfNotWhitelisted(); + throwIfNotWhitelisted(path); + fileInfo.MoveTo(path, overwrite); + } + /// + public Stream OpenRead() => Open(FileMode.Open, FileAccess.Read); + /// + public Stream OpenWrite() => Open(FileMode.OpenOrCreate, FileAccess.ReadWrite); + /// + public Stream Open(FileMode mode, FileAccess access) + { + if (mode != FileMode.Open || access != FileAccess.Read) throwIfNotWhitelisted(); + return fileInfo.Open(mode, access); + } + /// + public void Delete() + { + throwIfNotWhitelisted(); + fileInfo.Delete(); + } + + public override string ToString() => "DPFileInfo: " + Path; + + #region Private methods + + private void throwIfNotWhitelisted() + { + if (!Whitelisted) throw new OutOfScopeException(Path); + } + + private void throwIfNotWhitelisted(string path) + { + if (!Scope.IsFilePathWhitelisted(path)) throw new OutOfScopeException(path); + } + + internal override void Invalidate() + { + whitelisted = Scope.IsFilePathWhitelisted(Path); + } + private static IDPDirectoryInfo? tryCreateDirectoryInfo(FileInfo info, AbstractFileSystem fs) => + info.Directory is null ? null : new DPDirectoryInfo(info.Directory, fs); + private static IDPDirectoryInfo? tryCreateDirectoryInfo(IFileInfo info, AbstractFileSystem fs) => + info.Directory is null ? null : new DPDirectoryInfo(info.Directory, fs); + + #endregion + // The Preview methods are to check whether the operation is possible (ie: are we blacklisted or not?). + #region Preview methods + public bool PreviewCreate() => Whitelisted; + public bool PreviewDelete() => Whitelisted; + public bool PreviewOpen(FileMode mode, FileAccess access) => (mode == FileMode.Open && access == FileAccess.Read) || Whitelisted; + public bool PreviewMoveTo(string path, bool overwrite) => Whitelisted && Scope.IsFilePathWhitelisted(path); + public bool PreviewCopyTo(string path, bool overwrite) => Scope.IsFilePathWhitelisted(path); + #endregion + + // The Try methods are to perform the operation, and return whether it was successful or not. No errors are thrown. + #region Try methods + public bool TryCreate([NotNullWhen(true)] out Stream? stream) + { + stream = null; + if (!Whitelisted) return false; + try + { + stream = Create(); + return true; + } catch { } + return false; + } + public bool TryDelete() + { + if (!Whitelisted) return false; + try + { + Delete(); + return true; + } catch { return false; } + } + public bool TryMoveTo(string path, bool overwrite) + { + if (!Whitelisted && !Scope.IsFilePathWhitelisted(path)) return false; + try + { + MoveTo(path, overwrite); + return true; + } catch { return false; } + } + + public bool TryCopyTo(string path, bool overwrite, [NotNullWhen(true)] out IDPFileInfo? info) + { + info = null; + if (!Scope.IsFilePathWhitelisted(path)) return false; + try + { + info = CopyTo(path, overwrite); + return true; + } catch { return false; } + } + + public bool TryOpenRead([NotNullWhen(true)] out Stream? stream) => TryOpen(FileMode.Open, FileAccess.Read, out stream); + public bool TryOpenWrite([NotNullWhen(true)] out Stream? stream) => TryOpen(FileMode.OpenOrCreate, FileAccess.ReadWrite, out stream); + public bool TryOpen(FileMode mode, FileAccess access, [NotNullWhen(true)] out Stream? stream) + { + stream = null; + if (!Whitelisted) return false; + try + { + stream = Open(mode, access); + return true; + } catch { return false; } + } + + #endregion + + // The TryAndFix methods are to help fix common problems such as attemtping to fix an error due to an UnauthorizedAccessException + // which is caused by a hidden or read-only attribute. + #region TryAndFix methods + /// + /// Attempts to move the file, and if an exception is thrown (aside from ), it will attempt to fix the problem. + /// If it couldn't, it will be returned. + /// + /// Whether the operation successfully executed (whitelisted and no exception after attempted recovery). + public bool TryAndFixMoveTo(string path, bool overwrite, out Exception? exception) + { + exception = null; + if (!Whitelisted || !fileInfo.Exists) return false; + var targetInfo = FileSystem.CreateFileInfo(path); + if (targetInfo.Exists && (targetInfo.Attributes.HasFlag(FileAttributes.ReadOnly) || targetInfo.Attributes.HasFlag(FileAttributes.Hidden))) + { + try + { + targetInfo.Attributes = FileAttributes.Normal; + } catch (Exception ex) + { + exception = ex; + return false; + } + } + if (attemptWithRecovery(() => MoveTo(path, overwrite), this, out exception)) return true; + return false; + } + /// + /// Attempts to copy the file, and if an exception is thrown (aside from ), it will attempt to fix the problem. + /// If it couldn't, it will be returned. + /// + /// Whether the operation successfully executed (whitelisted and no exception after attempted recovery). + public bool TryAndFixCopyTo(string path, bool overwrite, out IDPFileInfo? info, out Exception? exception) + { + exception = null; + info = null; + if (!Whitelisted || !fileInfo.Exists) return false; + var targetInfo = FileSystem.CreateFileInfo(path); + if (targetInfo.Exists && (targetInfo.Attributes.HasFlag(FileAttributes.ReadOnly) || targetInfo.Attributes.HasFlag(FileAttributes.Hidden))) + { + try + { + targetInfo.Attributes = FileAttributes.Normal; + } + catch (Exception ex) + { + exception = ex; + return false; + } + } + IDPFileInfo? i = null; + if (!attemptWithRecovery(() => i = CopyTo(path, overwrite), this, out exception)) return false; + info = i; + return true; + } + /// + /// Attempts to open the file, and if an exception is thrown (aside from ), it will attempt to fix the problem. + /// If it couldn't, it will return the exception that followed. + /// + /// Whether the operation successfully executed (whitelisted and no exception after attempted recovery). + public bool TryAndFixOpen(FileMode mode, FileAccess access,[NotNullWhen(true)] out Stream? stream, [NotNullWhen(false)] out Exception? exception) + { + (exception, stream) = (null, null); + if (!Whitelisted) return false; + Stream? s = null; + if (attemptWithRecovery(() => s = Open(mode, access), this, out exception)) + { + stream = s; + return true; + } + return false; + } + /// + public bool TryAndFixOpenRead([NotNullWhen(true)] out Stream? stream, [NotNullWhen(false)] out Exception? exception) => TryAndFixOpen(FileMode.Open, FileAccess.Read, out stream, out exception); + /// + public bool TryAndFixOpenWrite([NotNullWhen(true)] out Stream? stream, [NotNullWhen(false)] out Exception? exception) => TryAndFixOpen(FileMode.OpenOrCreate, FileAccess.ReadWrite, out stream, out exception); + /// + /// Attempts to delete the file, and if an exception is thrown (aside from ), it will attempt to fix the problem. + /// If it couldn't, it will return the exception that followed. + /// + /// Whether the operation successfully executed (whitelisted and no exception after attempted recovery). + public bool TryAndFixDelete([NotNullWhen(false)] out Exception? exception) + { + exception = null; + if (!Whitelisted || !fileInfo.Exists) return false; + if (attemptWithRecovery(Delete, this, out exception)) return true; + return false; + } + #endregion + + private static bool attemptWithRecovery(Action a, IDPFileInfo info, [NotNullWhen(false)] out Exception? ex) + { + ex = null; + try + { + if (!info.Exists) return false; + a(); + return true; + } + catch (UnauthorizedAccessException e) + { + try + { + info.Attributes = FileAttributes.Normal; + } catch (Exception e2) + { + ex = new AggregateException(e, e2); + return false; + } + // Try again. + try + { + a(); return true; + } + catch (Exception e2) + { + ex = e2; + return false; + } + } + catch (Exception e) + { + ex = e; + return false; + } + } + } +} diff --git a/src/DAZ_Installer.IO/DPFileScopeSettings.cs b/src/DAZ_Installer.IO/DPFileScopeSettings.cs new file mode 100644 index 0000000..038ded5 --- /dev/null +++ b/src/DAZ_Installer.IO/DPFileScopeSettings.cs @@ -0,0 +1,169 @@ +using System.Collections.Immutable; +using System.IO; + +namespace DAZ_Installer.IO +{ + /// + /// Represents a file scope settings class that demonstrates whether files and/or directories are allowed to be modified (deleting, modifying) + /// and copying or deleting files. This class is used to ensure that the and operations are only allowed to approved directories. + /// This class DOES NOT change ANY OS settings (such as ACLs). It is a software-level protection mechanism. + /// + public class DPFileScopeSettings : IDPFileScopeSettings + { + /// + /// A file scope settings with no enforcement at all. + /// + public static readonly DPFileScopeSettings All = new(ImmutableList.Empty, ImmutableList.Empty, false, false, false, true); + /// + /// A file scope settings that does not accept anything. + /// + public static readonly DPFileScopeSettings None = CreateUltraStrict(ImmutableList.Empty, ImmutableList.Empty); + public readonly ImmutableHashSet WhitelistedDirectories; + public readonly ImmutableHashSet WhitelistedFilePaths; + + public bool ExplicitFilePaths { get; init; } = true; + public bool ExplicitDirectoryPaths { get; init; } = true; + public bool NoEnforcement { get; init; } = false; + public bool ThrowOnPathTransversal { get; init; } = false; + + /// + /// Creates a file scope settings with no enforcement. + /// + public DPFileScopeSettings() + { + WhitelistedFilePaths = WhitelistedDirectories = ImmutableHashSet.Empty; + ExplicitDirectoryPaths = ExplicitFilePaths = false; + NoEnforcement = true; + } + + /// + /// Copy constructor. + /// + public DPFileScopeSettings(DPFileScopeSettings other) + { + WhitelistedFilePaths = other.WhitelistedFilePaths; + WhitelistedDirectories = other.WhitelistedDirectories; + ExplicitDirectoryPaths = other.ExplicitDirectoryPaths; + ExplicitFilePaths = other.ExplicitFilePaths; + NoEnforcement = other.NoEnforcement; + ThrowOnPathTransversal = other.ThrowOnPathTransversal; + } + + /// + /// Creates a file scope settings with the specified whitelisted directories and files. Use this constructor when you already have setup the ImmutableHashSet to your liking. + /// This is a faster constructor compared to because it + /// needs to normalize all the paths before adding them to the ImmutableHashSet. + /// + /// The file paths to whitelist (optional). All paths should be full and sanitized./>. + /// The directory paths to whitelist (optional). + /// Determines whether to enforce operations must be in . If this is enabled with , then + /// the file path must also be within . + /// Determines whether to enforce operations must be explictly in . If this is enabled with , + /// then the file path must also be within . + /// Determines whether no enforcement should take place. This means that and will not be honored. + /// Determines whether to throw a during and if a path transversal is detected. + /// Does not check in or . + public DPFileScopeSettings(ImmutableHashSet filePaths, ImmutableHashSet dirs, bool strictDirectory = true, bool strictFile = false, bool throwOnPathTransversal = false, bool noEnforcement = false) + { + WhitelistedFilePaths = filePaths; + WhitelistedDirectories = dirs; + ExplicitDirectoryPaths = strictDirectory && !noEnforcement; + ExplicitFilePaths = strictFile && !noEnforcement; + NoEnforcement = noEnforcement; + ThrowOnPathTransversal = throwOnPathTransversal; + } + /// + /// Creates a file scope settings with the specified whitelisted directories and files and validates them if is enabled. + /// + /// The file paths to whitelist (optional). Paths will be sanitized. + /// The directory paths to whitelist (optional). Paths will be sanitized."/>. + /// Determines whether to enforce operations must be in . If this is enabled with , then + /// the file path must also be within . + /// Determines whether to enforce operations must be explictly in . If this is enabled with , + /// then the file path must also be within . + /// Determines whether no enforcement should take place. This means that and will not be honored. + /// Determines whether to throw a if a path transversal is detected. + public DPFileScopeSettings(IEnumerable filePaths, IEnumerable dirs, bool strictDirectory = true, bool strictFile = false, bool throwOnPathTransversal = false, bool noEnforcement = false) : + this(setupHashset(filePaths), setupHashset(dirs), strictDirectory, strictFile, throwOnPathTransversal, noEnforcement) { } + + /// + /// Creates an ultra strict file scope settings with the specified whitelisted directories and files. This is the strictest file scope settings possible. + /// + /// The file paths to whitelist (optional). Paths will be normalized via . + /// The file paths to whitelist (optional). Paths will be normalized via . + /// + public static DPFileScopeSettings CreateUltraStrict(IEnumerable filePaths, IEnumerable dirs) => new(filePaths, dirs, true, true, true, false); + + /// + /// Determines based on the current settings whether the specified directory is whitelisted. + /// + /// The path to check, will be normalized via + /// Whether the is whitelisted or not. + /// If is enabled and a path transversal is detected. + /// + public bool IsDirectoryWhitelisted(string directoryPath) + { + if (ThrowOnPathTransversal) PathTransversalException.ThrowIfTransversalDetected(directoryPath); + directoryPath = Path.GetFullPath(directoryPath); + if (NoEnforcement) return true; + if (ExplicitDirectoryPaths) + return WhitelistedDirectories.Contains(directoryPath); + + // When neither is enabled, then we just check if the directory is whitelisted. + if (WhitelistedDirectories.Contains(directoryPath)) return true; + foreach (var whitelistedDirectory in WhitelistedDirectories) + { + if (directoryPath.StartsWith(whitelistedDirectory, StringComparison.OrdinalIgnoreCase)) + return true; + } + return false; + } + + /// + /// Determines based on the curernt settings whether the specified file is whitelisted. + /// + /// + /// Whether the is whitelisted or not. + /// If is enabled and a path transversal is detected. + /// + public bool IsFilePathWhitelisted(string path) + { + if (ThrowOnPathTransversal) PathTransversalException.ThrowIfTransversalDetected(path); + path = Path.GetFullPath(path); + var dirPath = Path.GetDirectoryName(path) ?? string.Empty; + if (NoEnforcement) return true; + if (ExplicitFilePaths && !ExplicitDirectoryPaths) + return WhitelistedFilePaths.Contains(path); + if (ExplicitDirectoryPaths && !ExplicitFilePaths) + return WhitelistedDirectories.Contains(dirPath) || WhitelistedFilePaths.Contains(path); + if (ExplicitDirectoryPaths && ExplicitDirectoryPaths) + return WhitelistedFilePaths.Contains(path) + && WhitelistedDirectories.Contains(dirPath); + + // When none is enabled, then we check if the directory is whitelisted. + if (WhitelistedFilePaths.Contains(path) || WhitelistedDirectories.Contains(dirPath)) return true; + + foreach (var whitelistedDirectory in WhitelistedDirectories) + { + if (path.StartsWith(whitelistedDirectory, StringComparison.OrdinalIgnoreCase)) + return true; + } + return false; + } + + /// + /// Sets up the from the specified and normalizes the paths via . + /// + /// The to gather paths from. + /// A newly-made after processing . + private static ImmutableHashSet setupHashset(IEnumerable enumerable) + { + var builder = ImmutableHashSet.CreateBuilder(); + foreach (var str in enumerable) + { + builder.Add(Path.GetFullPath(PathHelper.NormalizePath(str))); + } + return builder.ToImmutable(); + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.IO/DPFileSystem.cs b/src/DAZ_Installer.IO/DPFileSystem.cs new file mode 100644 index 0000000..814dc79 --- /dev/null +++ b/src/DAZ_Installer.IO/DPFileSystem.cs @@ -0,0 +1,32 @@ +namespace DAZ_Installer.IO +{ + public class DPFileSystem : AbstractFileSystem + { + public override DPFileScopeSettings Scope { get; set; } = DPFileScopeSettings.None; + public static DPFileSystem Unrestricted => new(DPFileScopeSettings.All); + + public override void DeleteDirectory(string path, bool recursive = false) => Directory.Delete(path, recursive); + public override void DeleteFile(string path) => File.Delete(path); + public override IEnumerable EnumerateDirectories(string path) => Directory.EnumerateDirectories(path).Select(x => new DPDirectoryInfo(x, this)); + public override IEnumerable EnumerateFiles(string path) => Directory.EnumerateFiles(path).Select(x => new DPFileInfo(x, this)); + public override bool Exists(string? path, bool treatAsDirectory = false) => treatAsDirectory ? Directory.Exists(path) : File.Exists(path); + public override IDPDriveInfo[] GetDrives() => DriveInfo.GetDrives().Select(x => new DPDriveInfo(x, this)).ToArray(); + public override IDPFileInfo CreateFileInfo(string path) => new DPFileInfo(path, this); + protected internal override IDPFileInfo CreateFileInfo(string path, IDPDirectoryInfo? directory = null) => new DPFileInfo(new FileInfo(path), this, directory); + public override IDPDirectoryInfo CreateDirectoryInfo(string path) => new DPDirectoryInfo(new DirectoryInfo(path), this); + protected internal override IDPDirectoryInfo CreateDirectoryInfo(string path, IDPDirectoryInfo? parent) => new DPDirectoryInfo(new DirectoryInfo(path), this, parent); + public override IDPDriveInfo CreateDriveInfo(string path) => new DPDriveInfo(path, this); + + /// + /// Creates a new instance of with no access to the file system. + /// + public DPFileSystem() { } + /// + /// Creates a new instance of with the specified . + /// + /// The scope to use for all created and . + public DPFileSystem(DPFileScopeSettings scope) => Scope = scope; + + public DPFileSystem(DPFileSystem other) => Scope = new DPFileScopeSettings(other.Scope); + } +} diff --git a/src/DAZ_Installer.IO/DPIONodeBase.cs b/src/DAZ_Installer.IO/DPIONodeBase.cs new file mode 100644 index 0000000..bc2a81d --- /dev/null +++ b/src/DAZ_Installer.IO/DPIONodeBase.cs @@ -0,0 +1,13 @@ +namespace DAZ_Installer.IO +{ + public abstract class DPIONodeBase : IDPIONode + { + public abstract AbstractFileSystem FileSystem { get; internal set; } + public abstract string Name { get; } + public abstract string Path { get; } + public abstract bool Exists { get; } + public abstract bool Whitelisted { get; } + public abstract FileAttributes Attributes { get; set; } + internal abstract void Invalidate(); + } +} diff --git a/src/DAZ_Installer.IO/Extensions/DirectoryInfoExtensions.cs b/src/DAZ_Installer.IO/Extensions/DirectoryInfoExtensions.cs new file mode 100644 index 0000000..68c15d1 --- /dev/null +++ b/src/DAZ_Installer.IO/Extensions/DirectoryInfoExtensions.cs @@ -0,0 +1,15 @@ +namespace DAZ_Installer.IO +{ + public static class DirectoryInfoExtensions + { + /// + /// Converts the FileInfo into a by using an for scope context. + /// + /// The scope to use. + public static DPDirectoryInfo ToDPDirectoryInfo(this DirectoryInfo dir, AbstractFileSystem fs) + { + ArgumentNullException.ThrowIfNull(fs); + return new DPDirectoryInfo(dir, fs); + } + } +} diff --git a/src/DAZ_Installer.IO/Extensions/FileInfoExtensions.cs b/src/DAZ_Installer.IO/Extensions/FileInfoExtensions.cs new file mode 100644 index 0000000..29848c5 --- /dev/null +++ b/src/DAZ_Installer.IO/Extensions/FileInfoExtensions.cs @@ -0,0 +1,15 @@ +namespace DAZ_Installer.IO +{ + public static class FileInfoExtensions + { + /// + /// Converts the FileInfo into a by using an for file system context. + /// + /// The file system to use. + public static DPFileInfo ToDPFileInfo(this FileInfo file, AbstractFileSystem fs) + { + ArgumentNullException.ThrowIfNull(fs); + return new DPFileInfo(file, fs); + } + } +} diff --git a/src/DAZ_Installer.IO/OutOfScopeException.cs b/src/DAZ_Installer.IO/OutOfScopeException.cs new file mode 100644 index 0000000..f89364c --- /dev/null +++ b/src/DAZ_Installer.IO/OutOfScopeException.cs @@ -0,0 +1,16 @@ +namespace DAZ_Installer.IO +{ + /// + /// Represents an exception that is thrown when a path is out of scope from its and an action attempted to be performed on it. + /// + public class OutOfScopeException : Exception + { + /// + /// The offending file or directory path that was out of scope. + /// + public string OffendingPath { get; init; } + public OutOfScopeException(string path) : base($"Attempted to do an operation on a path that is out of scope: {path}") => OffendingPath = path; + public OutOfScopeException(string path, string? msg, bool template = true) : + base(template ? $"Attempted to do an operation on a path that is out of scope: {path}" : (msg ?? $"Attempted to do an operation on a path that is out of scope: {path}")) => OffendingPath = path; + } +} diff --git a/src/DAZ_Installer.IO/PathHelper.cs b/src/DAZ_Installer.IO/PathHelper.cs new file mode 100644 index 0000000..718af82 --- /dev/null +++ b/src/DAZ_Installer.IO/PathHelper.cs @@ -0,0 +1,258 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace DAZ_Installer.IO +{ + /// + /// Path helper methods designed for archive paths (not on-disk paths). + /// + public static class PathHelper + { + public const char DEFAULT_SEPERATOR = '/'; + /// + /// Returns a relative path using 's parent directory as a base to . + /// This function is used to get the path relative to a content folder. + /// This function returns the seperator of the path. + /// + /// The absolute path (or partial path) to compare. + /// The absolute path of the path to compare to minus the parent directory. + /// The relative path of the given path. + public static string GetRelativePathOfRelativeParent(ReadOnlySpan path, ReadOnlySpan relativeTo) + { + // String cannot end in a slash. + path = Path.TrimEndingDirectorySeparator(path); + relativeTo = Path.TrimEndingDirectorySeparator(relativeTo); + var rSeperator = GetSeperator(relativeTo); + var pSeperator = GetSeperator(path); + var pNameSections = path.ToString().Split(pSeperator); // i + var rNameSections = relativeTo.ToString().Split(rSeperator); // j + // We want find the last index of rNameSections + var findIndex = Array.IndexOf(pNameSections, rNameSections[^1]); + if (findIndex == -1) return path.ToString(); + var pathBuilder = new StringBuilder(path.Length); + for (var i = findIndex; i < pNameSections.Length; i++) + { + pathBuilder.Append(pNameSections[i]).Append(rSeperator); + } + return pathBuilder.Length == 0 ? string.Empty : pathBuilder.ToString().TrimEnd(rSeperator); + } + + /// + /// Returns the seperator of the given path. If there are multiple seperators, then it will return a forward slash. + /// If there are no seperators, it will return a forward slash. + /// + /// The path to determine the seperator from. + /// The seperator char + public static char GetSeperator(ReadOnlySpan path) + { + var backsSlash = path.LastIndexOf('\\') != -1; + var forwardSlash = path.LastIndexOf('/') != -1; + return backsSlash && !forwardSlash ? '\\' : '/'; + } + + /// + /// Returns the name of the last/parent/rightmost directory in the path depending on whether the path is a directory + /// path or not. + /// + /// The path you wish to get the "last" directory of. + /// Determines whether the path provided is a path to a directory or a file. + /// The name of the last directory. + public static string GetLastDir(string path, bool isFilePath) + { + path = Path.TrimEndingDirectorySeparator(path); + var seperator = GetSeperator(path); + var arr = path.Split(seperator); + string result; + if (isFilePath) + { + result = arr.Length switch + { + >= 2 when arr[^1].Contains('.') => arr[^2], + _ => string.Empty + }; + } else + result = arr.Length >= 2 ? path.Split(seperator)[^2] : string.Empty; + // Do not return anything if the result is a drive letter (eg: C:) + return result.EndsWith(':') ? string.Empty : result; + } + /// + /// Returns the parent directory of the given path. + /// + /// The path to get the parent of. + [Obsolete("Use PathHelper.Up() instead.", false)] + public static string GetParent(string path) => Up(path); + /// + /// Returns the file name of a path. + /// + /// The path to use. + public static string GetFileName(string path) + { + var seperator = GetSeperator(path); + return path.Split(seperator)[^1]; + } + + /// + /// Clean the directory path by ensuring a consistent seperator and removing the trailing seperator. + /// The only difference from is that it uses the seperator of the given path + /// (versus using the default seperator - forward slash). + /// + /// The path to process. + /// The path with no trailing seperator and consistent seperator. + public static string CleanDirPath(string path) + { + var seperator = GetSeperator(path); + path = SwitchToSeperator(path, seperator); + var strBuilder = new StringBuilder(path.Length); + foreach (var str in path.Split(seperator, options: StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) + { + strBuilder.Append(str).Append(seperator); + } + if (strBuilder.Length == 0) return string.Empty; + return strBuilder.Remove(strBuilder.Length - 1, 1).ToString(); + } + /// + /// Determines how many levels/directories above the given path is from the relative path. + /// + /// The path to see how many levels it is above . + /// The relative path to see how many levels the relative path is below . + /// The amount of levels/directorires above . + public static byte GetNumOfLevelsAbove(string path, string relativeTo) + { + var relPath = GetRelativePathOfRelativeParent(path, relativeTo); + var seperator = GetSeperator(relPath); + return (byte)relPath.Count((c) => c == seperator); + } + + /// + /// Returns the amount of subfolders/sub-directories in the given path. For example, Users\John\Documents would return 2. + /// Do not use for on-disk file paths. Use for archive paths. + /// + /// + /// + // TODO: Ensure that path does not end in a seperator. + public static byte GetSubfoldersCount(string path) + { + if (string.IsNullOrEmpty(path)) return 0; + var seperator = GetSeperator(path); + var c = path.Count((c) => c == seperator); + return Convert.ToByte(path[^1] == seperator ? c - 1 : c); + } + /// + /// Switches the seperators of the given path to the opposite seperator. For example, C:\Users\John\Documents would return C:/Users/John/Documents. + /// For paths that contain multiple seperators, it will make all seperators back-slashes. + /// + /// The path to switch seperators. + /// The path with seperators switched. + public static string SwitchSeperators(string path) + { + var seperator = GetSeperator(path); + var oppositeSeperator = seperator == '\\' ? '/' : '\\'; + return path.Replace(seperator, oppositeSeperator); + } + /// + /// Switches the seperators of the given path to the opposite seperator. For example, if you want to switch to backslashes, + /// C:/Users/John/Documents would return C:\Users\John\Documents. + /// + /// The path to switch seperators. + /// The seperator to switch to. + /// The path with seperators switched. + public static string SwitchToSeperator(string path, char seperator) + { + var oppositeSeperator = seperator == '\\' ? '/' : '\\'; + return path.Replace(oppositeSeperator, seperator); + } + + /// + /// GetDirectoryPath slightly differs from in a few scenarios.
+ /// If ends with a seperator, this function will return .
+ /// If contains only the drive (eg: C:\), this function will return ; whereas + /// would return C:.
+ /// If does not contain a seperator but has words, such as Documents, this function will return Documents; + /// whereas will return . + ///
+ /// The path to get the directory. + /// + public static string GetDirectoryPath(string path) + { + var strBuilder = new StringBuilder(path.Length); + var seperator = GetSeperator(path); + foreach (var str in path.Split(seperator, options: StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + strBuilder.Append(str).Append(seperator); + } + return CleanDirPath(strBuilder.ToString()); + } + + /// + /// Replaces all back slashes (\) with forward slashes (/) and trims the seperator if it is not a drive letter. + /// + /// The path to normalize. Cannot be null. + public static string NormalizePath(string path) + { + var s = path.Replace('\\', '/'); + // Issue #24: Prevent Path.GetFullPath from unexpectedly returning the current directory when the path is trimmed to a drive letter + // and a colon. For example: "D:/" -> "D:" then "D:" -> current directory. + if (s.Length >= 2 && s[^2] != ':') return s.TrimEnd('/'); + return s; + } + /// + /// Returns the full directory path of . It must be a directory path, not a file path. For example, + /// if is C:\Users\John\Documents, then C:\Users\John will be returned. + /// + /// A path of a directory. + /// The full directory path of . + public static string Up(string str) + { + if (string.IsNullOrWhiteSpace(str)) return str; + if (Path.EndsInDirectorySeparator(str)) + return str.Remove(str.LastIndexOf(GetLastDir(str, false))); + var fileName = Path.GetFileName(str); + return CleanDirPath(str.Remove(str.LastIndexOf(fileName))); + } + + /// + /// Returns the root directory of the given path in archive space. + /// For example, if is Content\data, then Content will be returned. + /// By default, the is assumed to be a directory path. + /// If it is a file path, then set to . + /// If is Content\data\file.txt, then Content will be returned if is . + /// If is the root directory (ex: Content), then is returned. + /// + /// The path to get the root directory of. + /// Determines whether the path provided is a path to a directory or a file. + /// The root directory of the path or if there is no root directory. + public static string GetRootDirectory(string path, bool isFilePath = false) + { + if (string.IsNullOrEmpty(path)) + return string.Empty; + + var separator = GetSeperator(path); + var pathParts = path.Split(separator); + + if (isFilePath && pathParts.Length > 1) + return pathParts[0]; + + if (!isFilePath && pathParts.Length > 0) + return pathParts[0]; + + return string.Empty; + } + /// + /// Checks for whether the given is attempting to directory tranverse. + /// + /// The path to check + /// if the path traverses, otherwise . + /// Throws when is . + public static bool CheckForTranversal(string path) + { + ArgumentNullException.ThrowIfNull(path); + // Normalize the path and split it. + foreach (var part in NormalizePath(path).Split(DEFAULT_SEPERATOR)) + { + if (part == ".." || part == ".") return true; + } + return false; + } + } +} diff --git a/src/DAZ_Installer.IO/PathTransversalException.cs b/src/DAZ_Installer.IO/PathTransversalException.cs new file mode 100644 index 0000000..45ba227 --- /dev/null +++ b/src/DAZ_Installer.IO/PathTransversalException.cs @@ -0,0 +1,20 @@ +namespace DAZ_Installer.IO +{ + public class PathTransversalException : Exception + { + public PathTransversalException() : base($"Path transversal detected") { } + public PathTransversalException(string msg) : base(msg) { } + public static void ThrowIfTransversalDetected(string path) + { + if (PathHelper.CheckForTranversal(path)) + throw new PathTransversalException(); + } + + public static void ThrowIfTransversalDetected(string path, string? messageIfDetected) + { + messageIfDetected ??= $"Path tranversal detected for {path}"; + if (PathHelper.CheckForTranversal(path)) + throw new PathTransversalException(messageIfDetected); + } + } +} diff --git a/src/DAZ_Installer.IO/Properties/AssemblyInfo1.cs b/src/DAZ_Installer.IO/Properties/AssemblyInfo1.cs new file mode 100644 index 0000000..26db163 --- /dev/null +++ b/src/DAZ_Installer.IO/Properties/AssemblyInfo1.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// In SDK-style projects such as this one, several assembly attributes that were historically +// defined in this file are now automatically added during build and populated with +// values defined in project properties. For details of which attributes are included +// and how to customise this process see: https://aka.ms/assembly-info-properties + + +// Setting ComVisible to false makes the types in this assembly not visible to COM +// components. If you need to access a type in this assembly from COM, set the ComVisible +// attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM. + +[assembly: Guid("7ef35820-2d92-43f0-866a-4efafdb6542c")] +[assembly: InternalsVisibleTo("DAZ_Installer.IOTests")] + diff --git a/src/DAZ_Installer.IO/Wrappers/DirectoryInfoWrapper.cs b/src/DAZ_Installer.IO/Wrappers/DirectoryInfoWrapper.cs new file mode 100644 index 0000000..7853150 --- /dev/null +++ b/src/DAZ_Installer.IO/Wrappers/DirectoryInfoWrapper.cs @@ -0,0 +1,40 @@ +namespace DAZ_Installer.IO.Wrappers +{ + /// + /// A wrapper that implements . + /// + public class DirectoryInfoWrapper : IDirectoryInfo + { + private readonly DirectoryInfo info; + private IDirectoryInfo? parent; + + internal DirectoryInfoWrapper(DirectoryInfo info) => this.info = info; + /// + public string Name => info.Name; + /// + public string FullName => info.FullName; + /// + public bool Exists => info.Exists; + /// + public FileAttributes Attributes { get => info.Attributes; set => info.Attributes = value; } + /// + public IDirectoryInfo? Parent => parent ??= tryCreateDirectoryInfoWrapper(); + /// + public void Create() => info.Create(); + /// + public void Delete(bool recursive) => info.Delete(recursive); + /// + public IEnumerable EnumerateDirectories() => info.EnumerateDirectories().Select(d => new DirectoryInfoWrapper(d)); + /// + public IEnumerable EnumerateDirectories(string pattern, EnumerationOptions options) => info.EnumerateDirectories(pattern, options).Select(d => new DirectoryInfoWrapper(d)); + /// + public IEnumerable EnumerateFiles() => info.EnumerateFiles().Select(f => new FileInfoWrapper(f)); + /// + public IEnumerable EnumerateFiles(string pattern, EnumerationOptions options) => info.EnumerateFiles(pattern, options).Select(f => new FileInfoWrapper(f)); + /// + public void MoveTo(string path) => info.MoveTo(path); + + public static implicit operator DirectoryInfoWrapper(DirectoryInfo info) => new DirectoryInfoWrapper(info); + private IDirectoryInfo? tryCreateDirectoryInfoWrapper() => info.Parent is null ? null : new DirectoryInfoWrapper(info.Parent); + } +} diff --git a/src/DAZ_Installer.IO/Wrappers/FileInfoWrapper.cs b/src/DAZ_Installer.IO/Wrappers/FileInfoWrapper.cs new file mode 100644 index 0000000..430b97c --- /dev/null +++ b/src/DAZ_Installer.IO/Wrappers/FileInfoWrapper.cs @@ -0,0 +1,40 @@ +namespace DAZ_Installer.IO.Wrappers +{ + /// + /// A wrapper that implements . + /// + public class FileInfoWrapper : IFileInfo + { + private readonly FileInfo info; + protected IDirectoryInfo? directory; + + internal FileInfoWrapper(FileInfo info) => this.info = info; + + /// + public string Name => info.Name; + /// + public string FullName => info.FullName; + /// + public bool Exists => info.Exists; + /// + public IDirectoryInfo? Directory => directory ??= tryCreateDirectoryWrapper(); + /// + public string? DirectoryName => info.DirectoryName; + /// + public FileAttributes Attributes { get => info.Attributes; set => info.Attributes = value; } + /// + public Stream Create() => info.Create(); + /// + public void Delete() => info.Delete(); + /// + public void MoveTo(string path, bool overwrite) => info.MoveTo(path, overwrite); + /// + public IFileInfo CopyTo(string path, bool overwrite) => new FileInfoWrapper(info.CopyTo(path, overwrite)); + /// + public Stream Open(FileMode mode, FileAccess access) => info.Open(mode, access); + + public static implicit operator FileInfoWrapper(FileInfo info) => new FileInfoWrapper(info); + + private IDirectoryInfo? tryCreateDirectoryWrapper() => info.Directory is null ? null : new DirectoryInfoWrapper(info.Directory); + } +} diff --git a/src/DAZ_Installer.IOTests/DAZ_Installer.IOTests.csproj b/src/DAZ_Installer.IOTests/DAZ_Installer.IOTests.csproj new file mode 100644 index 0000000..3305fb7 --- /dev/null +++ b/src/DAZ_Installer.IOTests/DAZ_Installer.IOTests.csproj @@ -0,0 +1,24 @@ + + + + net6.0 + enable + enable + + false + true + + + + + + + + + + + + + + + diff --git a/src/DAZ_Installer.IOTests/DPDirectoryInfoTests.cs b/src/DAZ_Installer.IOTests/DPDirectoryInfoTests.cs new file mode 100644 index 0000000..abffbc2 --- /dev/null +++ b/src/DAZ_Installer.IOTests/DPDirectoryInfoTests.cs @@ -0,0 +1,302 @@ +using DAZ_Installer.IO.Fakes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using System.Diagnostics; + +namespace DAZ_Installer.IO.Tests +{ + [TestClass] +#pragma warning disable CS0618 // FakeDirectoryInfo is obsolete for production, not testing. + public class DPDirectoryInfoTests + { + private static FakeFileSystem unlimitedCtx = new FakeFileSystem(DPFileScopeSettings.All); + private static FakeFileSystem noCtx = new FakeFileSystem(DPFileScopeSettings.None); + private static DPDirectoryInfo existingDir = null!; + private static DPDirectoryInfo nonexistantDir = null!; + private static DPDirectoryInfo outOfScope = null!; + private static DPDirectoryInfo destOutOfScope = null!; + private static string initExistingDirectoryPath = null!; + private static string initNonexistantDirectoryPath = null!; + private static string initOutOfScopeDirectoryPath = null!; + private static string initDestOutOfScopeDirectoryPath = null!; + private static string tempDir = null!; + + [ClassInitialize] + public static void ClassSetup(TestContext t) + { + tempDir = Path.Combine(t.DeploymentDirectory, "DAZ_Installer.IO.Tests.DPDirectoryInfoTests"); + + initExistingDirectoryPath = Path.Combine(tempDir, "exist"); + initNonexistantDirectoryPath = Path.Combine(tempDir, "nonexistant"); + initOutOfScopeDirectoryPath = Path.Combine(tempDir, "outofscope"); + initDestOutOfScopeDirectoryPath = Path.Combine(tempDir, "destoutofscope"); + } + + [TestInitialize] + public void TestInitialize() + { + existingDir = new DPDirectoryInfo(new FakeDirectoryInfo(initExistingDirectoryPath), unlimitedCtx); + nonexistantDir = new DPDirectoryInfo(new FakeDirectoryInfo(initNonexistantDirectoryPath), unlimitedCtx); + outOfScope = new DPDirectoryInfo(new FakeDirectoryInfo(initOutOfScopeDirectoryPath), noCtx); + destOutOfScope = new DPDirectoryInfo(new FakeDirectoryInfo(initOutOfScopeDirectoryPath), + new FakeFileSystem(DPFileScopeSettings.CreateUltraStrict(new string[] { initOutOfScopeDirectoryPath }, Array.Empty())) + ); + + } + + [TestMethod] + public void CreateTest() => existingDir.Create(); + [TestMethod] + public void CreateTest_OutOfScope() => Assert.ThrowsException(outOfScope.Create); + + [TestMethod] + public void DeleteTest() => existingDir.Delete(false); + [TestMethod] + public void DeleteTest_RecursiveNoSubdirs() + { + var fakeDirInfo = new FakeDirectoryInfo(initExistingDirectoryPath); + fakeDirInfo.Directories = new[] { new FakeDirectoryInfo(Path.Combine(initExistingDirectoryPath, "subdir")) }; + var dir = new DPDirectoryInfo(fakeDirInfo, unlimitedCtx); + dir.Delete(true); + + } + [TestMethod] + public void DeleteTest_OutOfScope() => Assert.ThrowsException(() => outOfScope.Delete(false)); + [TestMethod] + public void DeleteTest_OutOfScope_Recursive() + { + var fakeDirInfo = new FakeDirectoryInfo(initExistingDirectoryPath); + var path = Path.Combine(initExistingDirectoryPath, "subdir"); + fakeDirInfo.Directories = new[] { new FakeDirectoryInfo(path) }; + var dir = new DPDirectoryInfo(fakeDirInfo, new FakeFileSystem(DPFileScopeSettings.CreateUltraStrict(Array.Empty(), new[] { path }))); + Assert.ThrowsException(() => outOfScope.Delete(true)); + } + [TestMethod] + public void DeleteTest_SubDirOutOfScope_Recursive() + { + var fakeDirInfo = new FakeDirectoryInfo(initExistingDirectoryPath); + var subDirPath = Path.Combine(initExistingDirectoryPath, "subdir"); + fakeDirInfo.Files = new[] { new FakeFileInfo(Path.Combine(subDirPath, "a.txt")) }; + var dir = new DPDirectoryInfo(fakeDirInfo, new FakeFileSystem(DPFileScopeSettings.CreateUltraStrict(Array.Empty(), new[] { initExistingDirectoryPath }))); + Assert.ThrowsException(() => dir.Delete(true)); + } + + [TestMethod] + public void MoveToTest() => existingDir.MoveTo("anywhere"); + [TestMethod] + public void MoveToTest_Subdirs() + { + var fakeDirInfo = new FakeDirectoryInfo(initExistingDirectoryPath); + fakeDirInfo.Directories = new[] { new FakeDirectoryInfo(Path.Combine(initExistingDirectoryPath, "subdir")) }; + var dir = new DPDirectoryInfo(fakeDirInfo, unlimitedCtx); + dir.MoveTo("anywhere"); + } + [TestMethod] + public void MoveToTest_OutOfScope() => Assert.ThrowsException(() => outOfScope.MoveTo("anywhere")); + [TestMethod] + public void MoveToTest_OutOfScope_Subdirs() + { + var fakeDirInfo = new FakeDirectoryInfo(initExistingDirectoryPath); + var path = Path.Combine(initExistingDirectoryPath, "subdir"); + fakeDirInfo.Directories = new[] { new FakeDirectoryInfo(path) }; + var dir = new DPDirectoryInfo(fakeDirInfo, new FakeFileSystem(DPFileScopeSettings.CreateUltraStrict(Array.Empty(), new[] { path }))); + Assert.ThrowsException(() => outOfScope.MoveTo("anywhere")); + } + [TestMethod] + public void MoveToTest_SubDirOutOfScope() + { + var fakeDirInfo = new FakeDirectoryInfo(initExistingDirectoryPath); + fakeDirInfo.Directories = new[] { new FakeDirectoryInfo(Path.Combine(initExistingDirectoryPath, "subdir")) }; + var dir = new DPDirectoryInfo(fakeDirInfo, new FakeFileSystem(DPFileScopeSettings.CreateUltraStrict(Array.Empty(), new[] { initExistingDirectoryPath }))); + Assert.ThrowsException(() => dir.MoveTo("anywhere")); + } + + [TestMethod] + public void PreviewCreateTest() => Assert.IsTrue(existingDir.PreviewCreate()); + [TestMethod] + public void PreviewCreateTest_OutOfScope() => Assert.IsFalse(outOfScope.PreviewCreate()); + + [TestMethod] + public void PreviewDeleteTest() => Assert.IsTrue(existingDir.PreviewDelete(false)); + [TestMethod] + public void PreviewDeleteTest_OutOfScope() => Assert.IsFalse(outOfScope.PreviewDelete(false)); + + [TestMethod] + public void PreviewMoveToTest() => Assert.IsTrue(existingDir.PreviewMoveTo("anywhere")); + [TestMethod] + public void PreviewMoveToTest_Subdirs() + { + var fakeDirInfo = new FakeDirectoryInfo(initExistingDirectoryPath); + fakeDirInfo.Directories = new[] { new FakeDirectoryInfo(Path.Combine(initExistingDirectoryPath, "subdir")) }; + var dir = new DPDirectoryInfo(fakeDirInfo, unlimitedCtx); + Assert.IsTrue(dir.TryMoveTo("anywhere")); + } + [TestMethod] + public void PreviewMoveToTest_OutOfScope() => Assert.IsFalse(outOfScope.PreviewMoveTo("anywhere")); + [TestMethod] + public void PreviewMoveToTest_OutOfScope_Subdirs() + { + var fakeDirInfo = new FakeDirectoryInfo(initExistingDirectoryPath); + var path = Path.Combine(initExistingDirectoryPath, "subdir"); + fakeDirInfo.Directories = new[] { new FakeDirectoryInfo(path) }; + var dir = new DPDirectoryInfo(fakeDirInfo, new FakeFileSystem(DPFileScopeSettings.CreateUltraStrict(Array.Empty(), new[] { path }))); + Assert.IsFalse(dir.TryMoveTo("anywhere")); + } + [TestMethod] + public void PreviewMoveToTest_SubDirOutOfScope() + { + var fakeDirInfo = new FakeDirectoryInfo(initExistingDirectoryPath); + fakeDirInfo.Directories = new[] { new FakeDirectoryInfo(Path.Combine(initExistingDirectoryPath, "subdir")) }; + var dir = new DPDirectoryInfo(fakeDirInfo, new FakeFileSystem(DPFileScopeSettings.CreateUltraStrict(Array.Empty(), new[] { initExistingDirectoryPath }))); + Assert.IsFalse(dir.TryMoveTo("anywhere")); + } + + [TestMethod] + public void TryCreateTest() => Assert.IsTrue(existingDir.TryCreate()); + [TestMethod] + public void TryCreateTest_OutOfScope() => Assert.IsFalse(outOfScope.TryCreate()); + [TestMethod] + public void TryCreateTest_UnexpectedError() + { + var fakeDirInfo = new Mock("A:/Fake/Dir") { CallBase = true }; + fakeDirInfo.Setup(x => x.Create()).Throws(new Exception("gotcha bitch")); + var dir = new DPDirectoryInfo(fakeDirInfo.Object, unlimitedCtx, null); + Assert.IsFalse(dir.TryCreate()); + } + + [TestMethod] + public void TryDeleteTest() => Assert.IsTrue(existingDir.TryDelete(true)); + [TestMethod] + public void TryDeleteTest_OutOfScope() => Assert.IsFalse(outOfScope.TryDelete(true)); + [TestMethod] + public void TryDeleteTest_UnexpectedError() + { + var fakeDirInfo = new Mock("A:/Fake/Dir") { CallBase = true }; + fakeDirInfo.Setup(x => x.Delete(It.IsAny())).Throws(new Exception("gotcha bitch")); + var dir = new DPDirectoryInfo(fakeDirInfo.Object, unlimitedCtx, null); + Assert.IsFalse(dir.TryDelete(true)); + } + + [TestMethod] + public void TryMoveToTest() => Assert.IsTrue(existingDir.TryMoveTo("anywhere")); + [TestMethod] + public void TryMoveToTest_OutOfScope() => Assert.IsFalse(outOfScope.TryMoveTo("anywhere")); + [TestMethod] + public void TryMoveToTest_UnexpectedError() + { + var fakeDirInfo = new Mock("A:/Fake/Dir") { CallBase = true }; + fakeDirInfo.Setup(x => x.MoveTo("anywhere")).Throws(new Exception("gotcha bitch")); + var dir = new DPDirectoryInfo(fakeDirInfo.Object, unlimitedCtx, null); + Assert.IsFalse(dir.TryMoveTo("anywhere")); + } + [TestMethod] + public void FileSystemContextTest() + { + var scope = new DPFileScopeSettings(); + var ctx = new FakeFileSystem(scope); + var a = new DPDirectoryInfo(new FakeDirectoryInfo("a"), ctx); + Assert.AreSame(a.FileSystem, ctx); + } + + [TestMethod] + public void FileSystemContextTest_DirListedInContext() + { + var f = new FakeDirectoryInfo("Z://a"); + f.Parent = new FakeDirectoryInfo("Z://"); + var dir = new DPDirectoryInfo(f, new FakeFileSystem()); + Assert.AreSame(dir!.FileSystem, dir.Parent.FileSystem); + } + [TestMethod] + public void TryAndFixMoveToTest() + { + Assert.IsTrue(existingDir.TryAndFixMoveTo(Path.Combine(tempDir, "existing2"), out var ex)); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixMoveToTest_Blacklisted() + { + Assert.IsFalse(outOfScope.TryAndFixMoveTo(Path.Combine(tempDir, "existing2"), out var ex)); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixMoveToTest_NotExist() + { + var f = new FakeDirectoryInfo("Z://a"); + f.Exists = false; + var fs = new DPDirectoryInfo(f, noCtx); + + Assert.IsFalse(fs.TryAndFixMoveTo(Path.Combine(tempDir, "existing2"), out var ex)); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixMoveToTest_FixUnauthorizedSuccess() + { + var f = new Mock("Z://a") { CallBase = true }; + f.Object.Attributes = FileAttributes.Hidden | FileAttributes.ReadOnly; + f.Setup(x => x.MoveTo(It.IsAny())).Callback(() => + { + if (f.Object.Attributes.HasFlag(FileAttributes.ReadOnly) || f.Object.Attributes.HasFlag(FileAttributes.Hidden)) + throw new UnauthorizedAccessException(); + }); + var fs = new DPDirectoryInfo(f.Object, unlimitedCtx, null); + Assert.IsTrue(fs.TryAndFixMoveTo(Path.Combine(tempDir, "existing2"), out var ex)); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixMoveToTest_FixUnauthorizedFail() + { + var f = new Mock("Z://a") { CallBase = true }; + f.Object.Attributes = FileAttributes.Hidden | FileAttributes.ReadOnly; + f.Setup(x => x.MoveTo(It.IsAny())).Throws(new UnauthorizedAccessException()); + var fs = new DPDirectoryInfo(f.Object, unlimitedCtx, null); + Assert.IsFalse(fs.TryAndFixMoveTo(Path.Combine(tempDir, "existing2"), out var ex)); + Assert.IsNotNull(ex); + } + [TestMethod] + public void TryAndFixDeleteTest() + { + Assert.IsTrue(existingDir.TryAndFixDelete(true, out var ex)); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixDeleteTest_Blacklisted() + { + Assert.IsFalse(outOfScope.TryAndFixDelete(true, out var ex)); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixDeleteTest_NotExist() + { + var f = new FakeDirectoryInfo("Z://a"); + f.Exists = false; + var fs = new DPDirectoryInfo(f, noCtx); + + Assert.IsFalse(fs.TryAndFixDelete(true, out var ex)); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixDeleteTest_FixUnauthorizedSuccess() + { + var f = new Mock("Z://a") { CallBase = true }; + f.Object.Attributes = FileAttributes.Hidden | FileAttributes.ReadOnly; + f.Setup(x => x.Delete(It.IsAny())).Callback(() => + { + if (f.Object.Attributes.HasFlag(FileAttributes.ReadOnly) || f.Object.Attributes.HasFlag(FileAttributes.Hidden)) + throw new UnauthorizedAccessException(); + }); + var fs = new DPDirectoryInfo(f.Object, unlimitedCtx, null); + Assert.IsTrue(fs.TryAndFixDelete(true, out var ex)); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixDeleteTest_FixUnauthorizedFail() + { + var f = new Mock("Z://a") { CallBase = true }; + f.Object.Attributes = FileAttributes.Hidden | FileAttributes.ReadOnly; + f.Setup(x => x.Delete(It.IsAny())).Throws(new UnauthorizedAccessException()); + var fs = new DPDirectoryInfo(f.Object, unlimitedCtx, null); + Assert.IsFalse(fs.TryAndFixDelete(true, out var ex)); + Assert.IsNotNull(ex); + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.IOTests/DPFileInfoTests.cs b/src/DAZ_Installer.IOTests/DPFileInfoTests.cs new file mode 100644 index 0000000..b21eda5 --- /dev/null +++ b/src/DAZ_Installer.IOTests/DPFileInfoTests.cs @@ -0,0 +1,678 @@ +using DAZ_Installer.IO.Fakes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace DAZ_Installer.IO.Tests +{ + [TestClass] +#pragma warning disable CS0618 // This is for testing, as intended. + public class DPFileInfoTests + { + private static FakeFileSystem unlimitedCtx = new FakeFileSystem(DPFileScopeSettings.All); + private static FakeFileSystem noCtx = new FakeFileSystem(DPFileScopeSettings.None); + private static DPFileInfo existingFile = null!; + private static DPFileInfo nonexistantFile = null!; + private static DPFileInfo outOfScope = null!; + private static DPFileInfo destOutOfScope = null!; + private static string initExistingFilePath = null!; + private static string initNonexistantFilePath = null!; + private static string initOutOfScopeFilePath = null!; + private static string initDestOutOfScopeFilePath = null!; + private static string tempDir = null!; + + [ClassInitialize] + public static void ClassSetup(TestContext t) + { + tempDir = Path.Combine(t.DeploymentDirectory, "DAZ_Installer.IO.Tests.DPFileInfoTests"); + + initExistingFilePath = Path.Combine(tempDir, "exist.txt"); + initNonexistantFilePath = Path.Combine(tempDir, "nonexistant.txt"); + initOutOfScopeFilePath = Path.Combine(tempDir, "outofscope.txt"); + initDestOutOfScopeFilePath = Path.Combine(tempDir, "destoutofscope.txt"); + } + + [TestInitialize] + public void TestInitialize() + { + existingFile = new DPFileInfo(new FakeFileInfo(initExistingFilePath), unlimitedCtx); + nonexistantFile = new DPFileInfo(new FakeFileInfo(initNonexistantFilePath), unlimitedCtx); + outOfScope = new DPFileInfo(new FakeFileInfo(initOutOfScopeFilePath), noCtx); + destOutOfScope = new DPFileInfo(new FakeFileInfo(initOutOfScopeFilePath), + new FakeFileSystem(DPFileScopeSettings.CreateUltraStrict(new string[] { initOutOfScopeFilePath }, Array.Empty())) + ); + } + + [TestMethod] + public void CopyToTest_ExistingFile() + { + IDPFileInfo f = existingFile.CopyTo(Path.Combine(tempDir, "existing.txt"), true); + Assert.AreEqual(f.Path, Path.Combine(tempDir, "existing.txt")); + } + [TestMethod] + public void CopyToTest_OutOfScope_ExistingFile() + { + var tempFilePath = Path.Combine(tempDir, "existing.txt"); + var specificSettings = DPFileScopeSettings.CreateUltraStrict(new string[] { tempFilePath }, Array.Empty()); + var f = new DPFileInfo(existingFile.Path, new FakeFileSystem(specificSettings)); + Assert.ThrowsException(() => f.CopyTo(Path.Combine(tempDir, "existing.txt"), true)); + } + [TestMethod] + public void CopyToTest_NonexistantFile() => nonexistantFile.CopyTo(Path.Combine(tempDir, "nonexistant.txt"), true); + [TestMethod] + public void CopyToTest_OutOfScope_SourceFile() => Assert.ThrowsException(() => outOfScope.CopyTo(Path.Combine(tempDir, "existing.txt"), true)); + + [TestMethod] + public void CreateTest_ExistingFile() => existingFile.Create().Dispose(); + [TestMethod] + public void CreateTest_NonexistantFile() => nonexistantFile.Create().Dispose(); + [TestMethod] + public void CreateTest_OutOfScopeFile() => Assert.ThrowsException(() => outOfScope.Create().Dispose()); + + [TestMethod] + public void MoveToTest_ExistingFile() => existingFile.MoveTo(Path.Combine(tempDir, "existing2.txt"), true); + [TestMethod] + public void MoveToTest_SourceOutOfScope_ExistingFile() + { + var dest = Path.Combine(tempDir, "existing2.txt"); + var a = new DPFileInfo(existingFile.Path, + new FakeFileSystem(DPFileScopeSettings.CreateUltraStrict(new[] { dest }, new[] { Path.GetDirectoryName(dest)! })) + ); + Assert.ThrowsException(() => a.MoveTo(dest, true)); + } + [TestMethod] + public void MoveToTest_DestOutOfScope_ExistingFile() + { + var dest = Path.Combine(tempDir, "existing2.txt"); + var a = new DPFileInfo(existingFile.Path, + new FakeFileSystem(DPFileScopeSettings.CreateUltraStrict(new[] { existingFile.Path }, Array.Empty())) + ); + Assert.ThrowsException(() => a.MoveTo(dest, true)); + } + [TestMethod] + public void MoveToTest_NonexistantFile() + { + try + { + var dest = Path.Combine(tempDir, "existing2.txt"); + nonexistantFile.MoveTo(dest, true); + } + catch (OutOfScopeException ex) + { + Assert.Fail($"OutOfScopeException thrown when it should not have been: {ex}"); + } + catch { } + } + [TestMethod] + public void MoveToTest_OutOfScopeFile() => Assert.ThrowsException(() => outOfScope.MoveTo("", true)); + + [TestMethod] + public void OpenReadTest() + { + existingFile.OpenRead().Dispose(); + outOfScope.OpenRead().Dispose(); + try + { + nonexistantFile.OpenRead().Dispose(); + } + catch (OutOfScopeException ex) + { + Assert.Fail($"OutOfScopeException thrown when it should not have been: {ex}"); + } + catch { } + } + + [TestMethod] + public void OpenWriteTest_ExistingFile() => existingFile.OpenWrite().Dispose(); + + [TestMethod] + public void OpenWriteTest_NonexistantFile() => nonexistantFile.OpenWrite().Dispose(); + + [TestMethod] + public void OpenWriteTest_OutOfScopeFile() => Assert.ThrowsException(outOfScope.OpenWrite); + + [DataTestMethod] + [DataRow(FileMode.CreateNew, FileAccess.Read)] + [DataRow(FileMode.CreateNew, FileAccess.ReadWrite)] + [DataRow(FileMode.CreateNew, FileAccess.Write)] + [DataRow(FileMode.Create, FileAccess.Read)] + [DataRow(FileMode.Create, FileAccess.ReadWrite)] + [DataRow(FileMode.Create, FileAccess.Write)] + [DataRow(FileMode.Open, FileAccess.ReadWrite)] + [DataRow(FileMode.Open, FileAccess.Write)] + [DataRow(FileMode.Open, FileAccess.Read)] + [DataRow(FileMode.OpenOrCreate, FileAccess.ReadWrite)] + [DataRow(FileMode.OpenOrCreate, FileAccess.Write)] + [DataRow(FileMode.OpenOrCreate, FileAccess.Read)] + [DataRow(FileMode.Truncate, FileAccess.ReadWrite)] + [DataRow(FileMode.Truncate, FileAccess.Write)] + [DataRow(FileMode.Truncate, FileAccess.Read)] + [DataRow(FileMode.Append, FileAccess.ReadWrite)] + [DataRow(FileMode.Append, FileAccess.Write)] + [DataRow(FileMode.Append, FileAccess.Read)] + + public void OpenTest_ExistingFile_NoThrowUnauthorzied(FileMode mode, FileAccess access) + { + // we only care about if it throws OutOfScopeException by us, not the system. + try + { + existingFile.Open(mode, access).Dispose(); + } + catch (OutOfScopeException e) when (e.Message.Contains("Access to the path")) { Assert.Fail(); } + catch { } + } + + [DataTestMethod] + [DataRow(FileMode.CreateNew, FileAccess.Read)] + [DataRow(FileMode.CreateNew, FileAccess.ReadWrite)] + [DataRow(FileMode.CreateNew, FileAccess.Write)] + [DataRow(FileMode.Create, FileAccess.Read)] + [DataRow(FileMode.Create, FileAccess.ReadWrite)] + [DataRow(FileMode.Create, FileAccess.Write)] + [DataRow(FileMode.Open, FileAccess.ReadWrite)] + [DataRow(FileMode.Open, FileAccess.Write)] + [DataRow(FileMode.Open, FileAccess.Read)] + [DataRow(FileMode.OpenOrCreate, FileAccess.ReadWrite)] + [DataRow(FileMode.OpenOrCreate, FileAccess.Write)] + [DataRow(FileMode.OpenOrCreate, FileAccess.Read)] + [DataRow(FileMode.Truncate, FileAccess.ReadWrite)] + [DataRow(FileMode.Truncate, FileAccess.Write)] + [DataRow(FileMode.Truncate, FileAccess.Read)] + [DataRow(FileMode.Append, FileAccess.ReadWrite)] + [DataRow(FileMode.Append, FileAccess.Write)] + [DataRow(FileMode.Append, FileAccess.Read)] + public void OpenTest_Nonexistant_NoThrow(FileMode mode, FileAccess access) + { + // we only care about if it throws OutOfScopeException by us, not the system. + try + { + existingFile.Open(mode, access).Dispose(); + } + catch (OutOfScopeException e) when (e.Message.Contains("Access to the path")) { Assert.Fail(); } + catch { } + } + + [DataTestMethod] + [DataRow(FileMode.Open, FileAccess.ReadWrite)] + [DataRow(FileMode.Open, FileAccess.Write)] + [DataRow(FileMode.Open, FileAccess.Read)] + public void OpenTest_OutOfScope_NoThrow(FileMode mode, FileAccess access) + { + // we only care about if it throws OutOfScopeException by us, not the system. + try + { + outOfScope.Open(mode, access).Dispose(); + } + catch (OutOfScopeException e) when (e.Message.Contains("Access to the path")) { Assert.Fail(); } + catch { } + } + + [DataTestMethod] + [DataRow(FileMode.CreateNew, FileAccess.Read)] + [DataRow(FileMode.CreateNew, FileAccess.ReadWrite)] + [DataRow(FileMode.CreateNew, FileAccess.Write)] + [DataRow(FileMode.Create, FileAccess.Read)] + [DataRow(FileMode.Create, FileAccess.ReadWrite)] + [DataRow(FileMode.Create, FileAccess.Write)] + [DataRow(FileMode.Open, FileAccess.ReadWrite)] + [DataRow(FileMode.Open, FileAccess.Write)] + [DataRow(FileMode.OpenOrCreate, FileAccess.ReadWrite)] + [DataRow(FileMode.OpenOrCreate, FileAccess.Write)] + [DataRow(FileMode.OpenOrCreate, FileAccess.Read)] + [DataRow(FileMode.Truncate, FileAccess.ReadWrite)] + [DataRow(FileMode.Truncate, FileAccess.Write)] + [DataRow(FileMode.Truncate, FileAccess.Read)] + [DataRow(FileMode.Append, FileAccess.ReadWrite)] + [DataRow(FileMode.Append, FileAccess.Write)] + [DataRow(FileMode.Append, FileAccess.Read)] + public void OpenTest_OutOfScope_Throws(FileMode mode, FileAccess access) => + // we only care about if it throws OutOfScopeException by us, not the system. + Assert.ThrowsException(() => outOfScope.Open(mode, access)); + + [TestMethod] + public void DeleteTest_ExistingFile() => existingFile.Delete(); + [TestMethod] + public void DeleteTest_NonexistantFile() => nonexistantFile.Delete(); + [TestMethod] + public void DeleteTest_OutOfScopeFile() => Assert.ThrowsException(outOfScope.Delete); + [TestMethod] + public void PreviewCreateTest_ExistingFile() => Assert.IsTrue(existingFile.PreviewCreate()); + [TestMethod] + public void PreviewCreateTest_NonexistantFile() => Assert.IsTrue(nonexistantFile.PreviewCreate()); + [TestMethod] + public void PreviewCreateTest_OutOfScopeFile() => Assert.IsFalse(outOfScope.PreviewCreate()); + [TestMethod] + public void PreviewDeleteTest_ExistingFile() => Assert.IsTrue(existingFile.PreviewDelete()); + [TestMethod] + public void PreviewDeleteTest_NonexistantFile() => Assert.IsTrue(nonexistantFile.PreviewDelete()); + [TestMethod] + public void PreviewDeleteTest_OutOfScopeFile() => Assert.IsFalse(outOfScope.PreviewDelete()); + [DataTestMethod] + [DataRow(FileMode.CreateNew, FileAccess.Read)] + [DataRow(FileMode.CreateNew, FileAccess.ReadWrite)] + [DataRow(FileMode.CreateNew, FileAccess.Write)] + [DataRow(FileMode.Create, FileAccess.Read)] + [DataRow(FileMode.Create, FileAccess.ReadWrite)] + [DataRow(FileMode.Create, FileAccess.Write)] + [DataRow(FileMode.Open, FileAccess.ReadWrite)] + [DataRow(FileMode.Open, FileAccess.Write)] + [DataRow(FileMode.Open, FileAccess.Read)] + [DataRow(FileMode.OpenOrCreate, FileAccess.ReadWrite)] + [DataRow(FileMode.OpenOrCreate, FileAccess.Write)] + [DataRow(FileMode.OpenOrCreate, FileAccess.Read)] + [DataRow(FileMode.Truncate, FileAccess.ReadWrite)] + [DataRow(FileMode.Truncate, FileAccess.Write)] + [DataRow(FileMode.Truncate, FileAccess.Read)] + [DataRow(FileMode.Append, FileAccess.ReadWrite)] + [DataRow(FileMode.Append, FileAccess.Write)] + [DataRow(FileMode.Append, FileAccess.Read)] + public void PreviewOpenTest_ExistingFile(FileMode mode, FileAccess access) => Assert.IsTrue(existingFile.PreviewOpen(mode, access)); + [DataTestMethod] + [DataRow(FileMode.CreateNew, FileAccess.Read)] + [DataRow(FileMode.CreateNew, FileAccess.ReadWrite)] + [DataRow(FileMode.CreateNew, FileAccess.Write)] + [DataRow(FileMode.Create, FileAccess.Read)] + [DataRow(FileMode.Create, FileAccess.ReadWrite)] + [DataRow(FileMode.Create, FileAccess.Write)] + [DataRow(FileMode.Open, FileAccess.ReadWrite)] + [DataRow(FileMode.Open, FileAccess.Write)] + [DataRow(FileMode.Open, FileAccess.Read)] + [DataRow(FileMode.OpenOrCreate, FileAccess.ReadWrite)] + [DataRow(FileMode.OpenOrCreate, FileAccess.Write)] + [DataRow(FileMode.OpenOrCreate, FileAccess.Read)] + [DataRow(FileMode.Truncate, FileAccess.ReadWrite)] + [DataRow(FileMode.Truncate, FileAccess.Write)] + [DataRow(FileMode.Truncate, FileAccess.Read)] + [DataRow(FileMode.Append, FileAccess.ReadWrite)] + [DataRow(FileMode.Append, FileAccess.Write)] + [DataRow(FileMode.Append, FileAccess.Read)] + public void PreviewOpenTest_NonexistantFile(FileMode mode, FileAccess access) => Assert.IsTrue(nonexistantFile.PreviewOpen(mode, access)); + [DataTestMethod] + [DataRow(FileMode.Open, FileAccess.Read)] + public void PreviewOpenTest_OutOfScopeFile_True(FileMode mode, FileAccess access) => Assert.IsTrue(outOfScope.PreviewOpen(mode, access)); + [DataTestMethod] + [DataRow(FileMode.CreateNew, FileAccess.Read)] + [DataRow(FileMode.CreateNew, FileAccess.ReadWrite)] + [DataRow(FileMode.CreateNew, FileAccess.Write)] + [DataRow(FileMode.Create, FileAccess.Read)] + [DataRow(FileMode.Create, FileAccess.ReadWrite)] + [DataRow(FileMode.Create, FileAccess.Write)] + [DataRow(FileMode.Open, FileAccess.ReadWrite)] + [DataRow(FileMode.Open, FileAccess.Write)] + [DataRow(FileMode.OpenOrCreate, FileAccess.ReadWrite)] + [DataRow(FileMode.OpenOrCreate, FileAccess.Write)] + [DataRow(FileMode.OpenOrCreate, FileAccess.Read)] + [DataRow(FileMode.Truncate, FileAccess.ReadWrite)] + [DataRow(FileMode.Truncate, FileAccess.Write)] + [DataRow(FileMode.Truncate, FileAccess.Read)] + [DataRow(FileMode.Append, FileAccess.ReadWrite)] + [DataRow(FileMode.Append, FileAccess.Write)] + [DataRow(FileMode.Append, FileAccess.Read)] + public void PreviewOpenTest_OutOfScopeFile_False(FileMode mode, FileAccess access) => Assert.IsFalse(outOfScope.PreviewOpen(mode, access)); + [TestMethod] + public void PreviewMoveToTest_Scoped() => Assert.IsTrue(existingFile.PreviewMoveTo("anything", true)); + [TestMethod] + public void PreviewMoveToTest() => Assert.IsTrue(existingFile.PreviewMoveTo("anything", true)); + [TestMethod] + public void PreviewMoveToTest_SourceOutOfScope() => Assert.IsFalse(outOfScope.PreviewMoveTo("anything", true)); + [TestMethod] + public void PreviewMoveToTest_DestOutOfScope() => Assert.IsFalse(destOutOfScope.PreviewMoveTo("anything", true)); + + [TestMethod] + public void PreviewCopyToTest_SourceOutOfScope() => Assert.IsFalse(outOfScope.PreviewCopyTo("anything", true)); + [TestMethod] + public void PreviewCopyToTest_DestOutOfScope() => Assert.IsFalse(destOutOfScope.PreviewCopyTo("anything", true)); + + [TestMethod] + public void TryCreateTest() + { + Assert.IsTrue(existingFile.TryCreate(out Stream? c)); + c.Dispose(); + } + + [TestMethod] + public void TryCreateTest_OutOfScope() => Assert.IsFalse(outOfScope.TryCreate(out Stream? _)); + + [TestMethod] + public void TryDeleteTest() => Assert.IsTrue(existingFile.TryDelete()); + [TestMethod] + public void TryDeleteTest_OutOfScope() => Assert.IsFalse(outOfScope.TryDelete()); + + [TestMethod] + public void TryMoveToTest() => Assert.IsTrue(existingFile.TryMoveTo(Path.Combine(tempDir, "existing2.txt"), true)); + [TestMethod] + public void TryMoveToTest_SourceOutOfScope() => Assert.IsFalse(outOfScope.TryMoveTo(Path.Combine(tempDir, "existing2.txt"), true)); + [TestMethod] + public void TryMoveToTest_DestOutOfScope() => Assert.IsFalse(destOutOfScope.TryMoveTo(Path.Combine(tempDir, "existing2.txt"), true)); + + [TestMethod] + public void TryCopyToTest() => Assert.IsTrue(existingFile.TryCopyTo(Path.Combine(tempDir, "existing2.txt"), true, out IDPFileInfo? _)); + [TestMethod] + public void TryCopyToTest_SourceOutOfScope() => Assert.IsFalse(outOfScope.TryCopyTo(Path.Combine(tempDir, "existing2.txt"), true, out IDPFileInfo? _)); + [TestMethod] + public void TryCopyToTest_DestOutOfScope() => Assert.IsFalse(destOutOfScope.TryCopyTo(Path.Combine(tempDir, "existing2.txt"), true, out IDPFileInfo? _)); + + [TestMethod] + public void TryOpenReadTest() + { + Assert.IsTrue(existingFile.TryOpenRead(out Stream? c)); + c.Dispose(); + } + + [TestMethod] + public void TryOpenWriteTest() + { + Assert.IsTrue(existingFile.TryOpenWrite(out Stream? c)); + c.Dispose(); + } + [TestMethod] + public void TryOpenWriteTest_OutOfScope() => Assert.IsFalse(outOfScope.TryOpenWrite(out Stream? c)); + + [DataTestMethod] + [DataRow(FileMode.CreateNew, FileAccess.Read)] + [DataRow(FileMode.CreateNew, FileAccess.ReadWrite)] + [DataRow(FileMode.CreateNew, FileAccess.Write)] + [DataRow(FileMode.Create, FileAccess.Read)] + [DataRow(FileMode.Create, FileAccess.ReadWrite)] + [DataRow(FileMode.Create, FileAccess.Write)] + [DataRow(FileMode.Open, FileAccess.ReadWrite)] + [DataRow(FileMode.Open, FileAccess.Write)] + [DataRow(FileMode.Open, FileAccess.Read)] + [DataRow(FileMode.OpenOrCreate, FileAccess.ReadWrite)] + [DataRow(FileMode.OpenOrCreate, FileAccess.Write)] + [DataRow(FileMode.OpenOrCreate, FileAccess.Read)] + [DataRow(FileMode.Truncate, FileAccess.ReadWrite)] + [DataRow(FileMode.Truncate, FileAccess.Write)] + [DataRow(FileMode.Truncate, FileAccess.Read)] + [DataRow(FileMode.Append, FileAccess.ReadWrite)] + [DataRow(FileMode.Append, FileAccess.Write)] + [DataRow(FileMode.Append, FileAccess.Read)] + public void TryOpenTest(FileMode mode, FileAccess access) + { + try + { + existingFile.Open(mode, access).Dispose(); + } + catch (OutOfScopeException e) when (e.Message.Contains("Access to the path")) { Assert.Fail(); } + catch { } + } + [TestMethod] + [DataRow(FileMode.CreateNew, FileAccess.Read)] + [DataRow(FileMode.CreateNew, FileAccess.ReadWrite)] + [DataRow(FileMode.CreateNew, FileAccess.Write)] + [DataRow(FileMode.Create, FileAccess.Read)] + [DataRow(FileMode.Create, FileAccess.ReadWrite)] + [DataRow(FileMode.Create, FileAccess.Write)] + [DataRow(FileMode.Open, FileAccess.ReadWrite)] + [DataRow(FileMode.Open, FileAccess.Write)] + [DataRow(FileMode.OpenOrCreate, FileAccess.ReadWrite)] + [DataRow(FileMode.OpenOrCreate, FileAccess.Write)] + [DataRow(FileMode.OpenOrCreate, FileAccess.Read)] + [DataRow(FileMode.Truncate, FileAccess.ReadWrite)] + [DataRow(FileMode.Truncate, FileAccess.Write)] + [DataRow(FileMode.Truncate, FileAccess.Read)] + [DataRow(FileMode.Append, FileAccess.ReadWrite)] + [DataRow(FileMode.Append, FileAccess.Write)] + [DataRow(FileMode.Append, FileAccess.Read)] + public void TryOpenTest_OutOfScope_Throws(FileMode mode, FileAccess access) + { + try + { + outOfScope.Open(mode, access).Dispose(); + } + catch (OutOfScopeException e) when (e.Message.Contains("Access to the path")) { Assert.Fail(); } + catch { } + } + [DataTestMethod] + [DataRow(FileMode.Open, FileAccess.Read)] + public void TryOpenTest_OutOfScope_NoThrow(FileMode mode, FileAccess access) + { + try + { + outOfScope.Open(mode, access).Dispose(); + } + catch (OutOfScopeException e) when (e.Message.Contains("Access to the path")) { Assert.Fail(); } + catch { } + } + [TestMethod] + public void Directory_RetryOnNull() + { + var fake = new FakeFileInfo("D:/whoyomama/a.png"); + fake.Directory = new FakeDirectoryInfo("D:/whoyomama"); + var specific = new DPFileInfo(fake, noCtx, null); + Assert.IsNotNull(specific.Directory); + } + [TestMethod] + public void ContextTest() + { + var scope = new DPFileScopeSettings(); + var ctx = new FakeFileSystem(scope); + var a = new DPFileInfo(new FakeFileInfo("a.txt"), ctx); + Assert.AreSame(a.FileSystem, ctx); + } + [TestMethod] + public void ContextTest_ContextChanged() + { + existingFile.FileSystem = noCtx; + Assert.AreSame(noCtx, existingFile.FileSystem); + } + [TestMethod] + public void ContextTest_DirListedInContext() + { + var f = new FakeFileInfo("Z://a.txt"); + f.Directory = new FakeDirectoryInfo("Z://"); + var file = new DPFileInfo(f, noCtx); + Assert.AreSame(file!.FileSystem, file.Directory.FileSystem); + } + [TestMethod] + public void ScopeTest() + { + var scope = new DPFileScopeSettings(); + var ctx = new FakeFileSystem(scope); + var a = new DPFileInfo(new FakeFileInfo("a.txt"), ctx); + Assert.AreSame(a.Scope, scope); + } + + [TestMethod] + public void TryAndFixMoveToTest() + { + Assert.IsTrue(existingFile.TryAndFixMoveTo(Path.Combine(tempDir, "existing2.txt"), true, out var ex)); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixMoveToTest_Blacklisted() + { + Assert.IsFalse(outOfScope.TryAndFixMoveTo(Path.Combine(tempDir, "existing2.txt"), true, out var ex)); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixMoveToTest_NotExist() + { + var f = new FakeFileInfo("Z://a.txt"); + f.Exists = false; + var fs = new DPFileInfo(f, noCtx); + + Assert.IsFalse(fs.TryAndFixMoveTo(Path.Combine(tempDir, "existing2.txt"), true, out var ex)); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixMoveToTest_FixUnauthorizedSuccess() + { + var f = new Mock("Z://a.txt") { CallBase = true }; + f.Object.Attributes = FileAttributes.Hidden | FileAttributes.ReadOnly; + f.Setup(x => x.MoveTo(It.IsAny(), It.IsAny())).Callback(() => + { + if (f.Object.Attributes.HasFlag(FileAttributes.ReadOnly) || f.Object.Attributes.HasFlag(FileAttributes.Hidden)) + throw new UnauthorizedAccessException(); + }); + var fs = new DPFileInfo(f.Object, unlimitedCtx, null); + Assert.IsTrue(fs.TryAndFixMoveTo(Path.Combine(tempDir, "existing2.txt"), true, out var ex)); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixMoveToTest_FixUnauthorizedFail() + { + var f = new Mock("Z://a.txt") { CallBase = true }; + f.Object.Attributes = FileAttributes.Hidden | FileAttributes.ReadOnly; + f.Setup(x => x.MoveTo(It.IsAny(), It.IsAny())).Throws(new UnauthorizedAccessException()); + var fs = new DPFileInfo(f.Object, unlimitedCtx, null); + Assert.IsFalse(fs.TryAndFixMoveTo(Path.Combine(tempDir, "existing2.txt"), true, out var ex)); + Assert.IsNotNull(ex); + } + [TestMethod] + public void TryAndFixCopyToTest() + { + Assert.IsTrue(existingFile.TryAndFixCopyTo(Path.Combine(tempDir, "existing2.txt"), true, out var stream, out var ex)); + Assert.IsNotNull(stream); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixCopyToTest_Blacklisted() + { + Assert.IsFalse(outOfScope.TryAndFixCopyTo(Path.Combine(tempDir, "existing2.txt"), true, out var stream, out var ex)); + Assert.IsNull(ex); + Assert.IsNull(stream); + + } + [TestMethod] + public void TryAndFixCopyToTest_NotExist() + { + var f = new FakeFileInfo("Z://a.txt"); + f.Exists = false; + var fs = new DPFileInfo(f, noCtx); + + Assert.IsFalse(fs.TryAndFixCopyTo(Path.Combine(tempDir, "existing2.txt"), true, out var stream, out var ex)); + Assert.IsNull(ex); + Assert.IsNull(stream); + } + [TestMethod] + public void TryAndFixCopyToTest_FixUnauthorizedSuccess() + { + var f = new Mock("Z://a.txt") { CallBase = true }; + f.Object.Attributes = FileAttributes.Hidden | FileAttributes.ReadOnly; + f.Setup(x => x.CopyTo(It.IsAny(), It.IsAny())).Returns(() => + { + if (f.Object.Attributes.HasFlag(FileAttributes.ReadOnly) || f.Object.Attributes.HasFlag(FileAttributes.Hidden)) + throw new UnauthorizedAccessException(); + return new FakeFileInfo("Z://a.txt"); + }); + var fs = new DPFileInfo(f.Object, unlimitedCtx, null); + + Assert.IsTrue(fs.TryAndFixCopyTo(Path.Combine(tempDir, "existing2.txt"), true, out var stream, out var ex)); + Assert.IsNull(ex); + Assert.IsNotNull(stream); + } + [TestMethod] + public void TryAndFixCopyToTest_FixUnauthorizedFail() + { + var f = new Mock("Z://a.txt") { CallBase = true }; + f.Object.Attributes = FileAttributes.Hidden | FileAttributes.ReadOnly; + f.Setup(x => x.CopyTo(It.IsAny(), It.IsAny())).Throws(new UnauthorizedAccessException()); + var fs = new DPFileInfo(f.Object, unlimitedCtx, null); + Assert.IsFalse(fs.TryAndFixCopyTo(Path.Combine(tempDir, "existing2.txt"), true, out var stream, out var ex)); + Assert.IsNotNull(ex); + Assert.IsNull(stream); + } + [TestMethod] + public void TryAndFixOpenTest() + { + Assert.IsTrue(existingFile.TryAndFixOpen(FileMode.Open, FileAccess.Read, out var stream, out var ex)); + Assert.IsNull(ex); + Assert.IsNotNull(stream); + } + [TestMethod] + public void TryAndFixOpenTest_Blacklisted() + { + Assert.IsFalse(outOfScope.TryAndFixOpen(FileMode.Open, FileAccess.Read, out var stream, out var ex)); + Assert.IsNull(ex); + Assert.IsNull(stream); + } + [TestMethod] + public void TryAndFixOpenTest_NotExist() + { + var f = new FakeFileInfo("Z://a.txt"); + f.Exists = false; + var fs = new DPFileInfo(f, noCtx); + + Assert.IsFalse(fs.TryAndFixOpen(FileMode.Open, FileAccess.Read, out var stream, out var ex)); + Assert.IsNull(stream); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixOpenTest_FixUnauthorizedSuccess() + { + var f = new Mock("Z://a.txt") { CallBase = true }; + f.Object.Attributes = FileAttributes.Hidden | FileAttributes.ReadOnly; + f.Setup(x => x.Open(It.IsAny(), It.IsAny())).Returns(() => + { + if (f.Object.Attributes.HasFlag(FileAttributes.ReadOnly) || f.Object.Attributes.HasFlag(FileAttributes.Hidden)) + throw new UnauthorizedAccessException(); + return Stream.Null; + }); + var fs = new DPFileInfo(f.Object, unlimitedCtx, null); + + Assert.IsTrue(fs.TryAndFixOpen(FileMode.Open, FileAccess.Read, out var stream, out var ex)); + Assert.IsNotNull(stream); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixOpenTest_FixUnauthorizedFail() + { + var f = new Mock("Z://a.txt") { CallBase = true }; + f.Object.Attributes = FileAttributes.Hidden | FileAttributes.ReadOnly; + f.Setup(x => x.Open(It.IsAny(), It.IsAny())).Throws(new UnauthorizedAccessException()); + var fs = new DPFileInfo(f.Object, unlimitedCtx, null); + + Assert.IsFalse(fs.TryAndFixOpen(FileMode.Open, FileAccess.Read, out var stream, out var ex)); + Assert.IsNull(stream); + Assert.IsNotNull(ex); + } + [TestMethod] + public void TryAndFixDeleteTest() + { + Assert.IsTrue(existingFile.TryAndFixDelete(out var ex)); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixDeleteTest_Blacklisted() + { + Assert.IsFalse(outOfScope.TryAndFixDelete(out var ex)); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixDeleteTest_NotExist() + { + var f = new FakeFileInfo("Z://a.txt"); + f.Exists = false; + var fs = new DPFileInfo(f, noCtx); + + Assert.IsFalse(fs.TryAndFixDelete(out var ex)); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixDeleteTest_FixUnauthorizedSuccess() + { + var f = new Mock("Z://a.txt") { CallBase = true }; + f.Object.Attributes = FileAttributes.Hidden | FileAttributes.ReadOnly; + f.Setup(x => x.Delete()).Callback(() => + { + if (f.Object.Attributes.HasFlag(FileAttributes.ReadOnly) || f.Object.Attributes.HasFlag(FileAttributes.Hidden)) + throw new UnauthorizedAccessException(); + }); + var fs = new DPFileInfo(f.Object, unlimitedCtx, null); + + Assert.IsTrue(fs.TryAndFixDelete(out var ex)); + Assert.IsNull(ex); + } + [TestMethod] + public void TryAndFixDeleteTest_FixUnauthorizedFail() + { + var f = new Mock("Z://a.txt") { CallBase = true }; + f.Object.Attributes = FileAttributes.Hidden | FileAttributes.ReadOnly; + f.Setup(x => x.Delete()).Throws(new UnauthorizedAccessException()); + var fs = new DPFileInfo(f.Object, unlimitedCtx, null); + + Assert.IsFalse(fs.TryAndFixDelete(out var ex)); + Assert.IsNotNull(ex); + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.IOTests/DPFileScopeSettingsTests.cs b/src/DAZ_Installer.IOTests/DPFileScopeSettingsTests.cs new file mode 100644 index 0000000..abdf7cb --- /dev/null +++ b/src/DAZ_Installer.IOTests/DPFileScopeSettingsTests.cs @@ -0,0 +1,331 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting.Logging; + +namespace DAZ_Installer.IO.Tests +{ + [TestClass] + public class DPFileScopeSettingsTests + { + public static readonly string tempPath = Path.Combine(Path.GetTempPath(), "DAZ_Installer.IO.Tests"); + public static readonly string tempPathA = Path.Combine(tempPath, "a.txt"); + public static readonly string nonExistantRootPath = GetRandomPathRoot(); + public static IEnumerable ConstructorSource => new string[][][] + { + // Directories Files WantDirectories WantFiles + new string[][] { new[] { "Z:\\3D\\My 3D Library" }, new[] { "Z:\\3D\\My 3D Library\\a.txt" }, new[] { "Z:\\3D\\My 3D Library" }, new[] { "Z:\\3D\\My 3D Library\\a.txt" } }, // Test case 1 - Nonexistant on disk. + new string[][] { new[] { tempPath }, new[] { tempPathA } , new[] { tempPath }, new[] { tempPathA } }, // Test case 2 - Exists on disk. + new string[][] { new[] { "Z:\\3D\\My 3D Library" }, Array.Empty() , new[] { "Z:\\3D\\My 3D Library" }, Array.Empty() }, // Test case 3 - No files listed. + new string[][] { Array.Empty() , new[] { "Z:\\3D\\My 3D Library\\a.txt" }, Array.Empty() , new[] { "Z:\\3D\\My 3D Library\\a.txt" } }, // Test case 4 - No dirs listed. + new string[][] { new[] { "Z:/3D/My 3D Library" } , new[] { "Z:/3D/My 3D Library/a.txt" } , new[] { "Z:\\3D\\My 3D Library" }, new[] { "Z:\\3D\\My 3D Library\\a.txt" } }, // Test case 5 - Different seperator. + + }; + [ClassInitialize] + public static void SetupClass(TestContext t) => Directory.CreateDirectory(tempPath); + + [ClassCleanup] + public static void CleanupClass() + { + try + { + Directory.Delete(tempPath, true); + } + catch { Logger.LogMessage("Failed to delete tempPath."); } + } + + [TestMethod] + [DynamicData(nameof(ConstructorSource))] + public void DPFileScopeSettingsTest_IEnumerableConversion(string[] dirs, string[] files, string[] wantDirs, string[] wantFiles) + { + var b = new DPFileScopeSettings(files, dirs); + CollectionAssert.AreEqual(b.WhitelistedDirectories.ToArray(), wantDirs); + CollectionAssert.AreEqual(b.WhitelistedFilePaths.ToArray(), wantFiles); + } + + private static string GetPath(string path) + { + if (path[0] == '~') return Path.Join(Directory.GetDirectoryRoot(tempPath), path[1..]); + else if (path[0] == '!') return Path.Join(nonExistantRootPath, path[1..]); + else if (path.Length >= 2 && path[0] == '.' && path[1] != '.') return Path.Join(tempPath, path); + else return path; + } + + /// + /// Returns a random path root that is not the same as the current directory (even if it does not exist). + /// + /// A random drive letter with the colon and slash after it. + public static string GetRandomPathRoot() + { + var currentDrive = Path.GetPathRoot(Directory.GetCurrentDirectory())![0]; + var availableDrives = Enumerable.Range('A', 'Z' - 'A' + 1) + .Select(c => (char)c) + .Where(c => c != currentDrive).ToList(); + return availableDrives[new Random().Next(availableDrives.Count)].ToString() + ":/"; + } + + [TestMethod] + [DataRow("~My Private Info/OMG/Plz No"), DataRow("~My Private Info\\OMG\\Plz No")] + [DataRow(".."), DataRow("../"), DataRow("..\\")] + [DataRow("~.."), DataRow("~../"), DataRow("~..\\")] + [DataRow("../../Windows"), DataRow("..\\..\\Windows")] + [DataRow("./Windows"), DataRow(".\\Windows")] + [DataRow(".\\"), DataRow("./"), DataRow(".\\")] + [DataRow("top secret/../../../Windows"), DataRow("top secret\\..\\..\\..\\Windows")] + [DataRow("top secret.jpg\\..\\..\\..\\Windows"), DataRow("top secret.jpg//..//..//..\\Windows")] + [DataRow("%2e%2e%2f"), DataRow("%2e%2e%5c")] + [DataRow(".\\%2e%2e%2f"), DataRow("../%2e%2e%2f")] + + + public void IsDirectoryWhitelistedTest_DenyOnDefault(string path) + { + Logger.LogMessage($"FileInfo Interpret: {new FileInfo(GetPath(path)).FullName}"); + var defaultScope = new DPFileScopeSettings(Array.Empty(), new string[] { tempPath }); + path = GetPath(path); + Assert.IsFalse(defaultScope.IsDirectoryWhitelisted(path)); + } + + [TestMethod] + [DataRow("~My Private Info/OMG/Plz No"), DataRow("~My Private Info\\OMG\\Plz No")] + [DataRow(".."), DataRow("../"), DataRow("..\\")] + [DataRow("../../Windows"), DataRow("..\\..\\Windows")] + [DataRow("./Windows"), DataRow(".\\Windows")] + [DataRow(".\\"), DataRow("./"), DataRow(".\\")] + [DataRow("top secret/../../../Windows"), DataRow("top secret\\..\\..\\..\\Windows")] + [DataRow("top secret.jpg\\..\\..\\..\\Windows"), DataRow("top secret.jpg//..//..//..\\Windows")] + [DataRow("%2e%2e%2f"), DataRow("%2e%2e%5c")] + [DataRow(".\\%2e%2e%2f"), DataRow("../%2e%2e%2f")] + public void IsDirectoryWhitelistedTest_DenyOnStrict(string path) + { + var defaultScope = new DPFileScopeSettings(Array.Empty(), new string[] { tempPath }, true, true); + path = GetPath(path); + Assert.IsFalse(defaultScope.IsDirectoryWhitelisted(path)); + } + + [TestMethod] + [DataRow("~My Private Info/OMG/Plz No"), DataRow("~My Private Info\\OMG\\Plz No")] + [DataRow(".."), DataRow("../"), DataRow("..\\")] + [DataRow("../../Windows"), DataRow("..\\..\\Windows")] + [DataRow("./Windows"), DataRow(".\\Windows")] + [DataRow(".\\"), DataRow("./"), DataRow(".\\")] + [DataRow("top secret/../../../Windows"), DataRow("top secret\\..\\..\\..\\Windows")] + [DataRow("top secret.jpg\\..\\..\\..\\Windows"), DataRow("top secret.jpg//..//..//..\\Windows")] + [DataRow("%2e%2e%2f"), DataRow("%2e%2e%5c")] + [DataRow(".\\%2e%2e%2f"), DataRow("../%2e%2e%2f")] + public void IsDirectoryWhitelistedTest_DenyOnStrictFiles(string path) + { + var defaultScope = new DPFileScopeSettings(new string[] { GetPath("!a.txt") }, Array.Empty(), false, true); + path = GetPath(path); + Assert.IsFalse(defaultScope.IsDirectoryWhitelisted(path)); + } + + [TestMethod] + [DataRow("~"), DataRow("~")] + [DataRow("~Winners"), DataRow("~Winners")] + + + public void IsDirectoryWhitelistedTest_AcceptOnDefault(string path) + { + var defaultScope = new DPFileScopeSettings(Array.Empty(), new string[] { GetPath("~"), GetPath("~Winners") }); + path = GetPath(path); + Logger.LogMessage($"Path: {path}"); + Assert.IsTrue(defaultScope.IsDirectoryWhitelisted(path)); + } + + [TestMethod] + [DataRow("~"), DataRow("~")] + [DataRow("~Winners"), DataRow("~Winners")] + + public void IsDirectoryWhitelistedTest_AcceptOnStrict(string path) + { + var defaultScope = new DPFileScopeSettings(Array.Empty(), new string[] { GetPath("~"), GetPath("~Winners") }, true, true); + path = GetPath(path); + Assert.IsTrue(defaultScope.IsDirectoryWhitelisted(path)); + } + + [TestMethod] + [DataRow("~"), DataRow("~")] + [DataRow("~Winners"), DataRow("~Winners")] + [DataRow("!Winners"), DataRow("!Winners")] + [DataRow("../../"), DataRow("..\\..")] + + + public void IsDirectoryWhitelistedTest_AcceptOnNoEnforcement(string path) + { + var defaultScope = new DPFileScopeSettings(Array.Empty(), Array.Empty(), true, true, false, true); + path = GetPath(path); + Assert.IsTrue(defaultScope.IsDirectoryWhitelisted(path)); + } + + [TestMethod] + [DataRow(".."), DataRow("../"), DataRow("..\\")] + [DataRow("~.."), DataRow("~../"), DataRow("~..\\")] + [DataRow("../../Windows"), DataRow("..\\..\\Windows")] + [DataRow("../Windows"), DataRow("..\\Windows")] + [DataRow("top secret/../../../Windows"), DataRow("top secret\\..\\..\\..\\Windows")] + [DataRow("top secret.jpg\\..\\..\\..\\Windows"), DataRow("top secret.jpg//..//..//..\\Windows")] + public void IsDirectoryWhitelistedTest_ThrowOnPathTransversal(string path) + { + var defaultScope = new DPFileScopeSettings(Array.Empty(), Array.Empty(), true, true, true); + path = GetPath(path); + Assert.ThrowsException(() => defaultScope.IsDirectoryWhitelisted(path)); + } + + [TestMethod] + [DataRow("~My Private Info/OMG/Plz No/b.txt"), DataRow("~My Private Info\\OMG\\Plz No\\b.txt")] + [DataRow(".."), DataRow("../"), DataRow("..\\")] + [DataRow("../../Windows"), DataRow("..\\..\\Windows")] + [DataRow("./Windows/exploit.exe"), DataRow(".\\Windows\\exploit.exe")] + [DataRow(".\\"), DataRow("./"), DataRow(".\\")] + [DataRow(".\\exploit.exe"), DataRow("./exploit.exe"), DataRow(".\\exploit.exe")] + + [DataRow("top secret/../../../Windows"), DataRow("top secret\\..\\..\\..\\Windows")] + [DataRow("top secret.jpg\\..\\..\\..\\Windows"), DataRow("top secret.jpg//..//..//..\\Windows")] + [DataRow("%2e%2e%2f"), DataRow("%2e%2e%5c")] + [DataRow(".\\%2e%2e%2f"), DataRow("../%2e%2e%2f")] + public void IsFilePathWhitelistedTest_DenyOnDefault(string path) + { + var defaultScope = new DPFileScopeSettings(new string[] { GetPath("~My Private Info/OMG/Plz No/a.txt"), GetPath("!a.txt" )}, new string[] { GetPath("~") }); + path = GetPath(path); + Assert.IsFalse(defaultScope.IsFilePathWhitelisted(path)); + } + + [TestMethod] + [DataRow("~My Private Info/OMG/Plz No/a.txt"), DataRow("~My Private Info\\OMG\\Plz No\\a.txt")] + [DataRow("~My Private Info/OMG/Plz No/b.txt"), DataRow("~My Private Info\\OMG\\Plz No\\b.txt")] + [DataRow(".."), DataRow("../"), DataRow("..\\")] + [DataRow("../../Windows"), DataRow("..\\..\\Windows")] + [DataRow("./Windows/exploit.exe"), DataRow(".\\Windows\\exploit.exe")] + [DataRow(".\\"), DataRow("./"), DataRow(".\\")] + [DataRow(".\\exploit.exe"), DataRow("./exploit.exe"), DataRow(".\\exploit.exe")] + [DataRow("top secret/../../../Windows"), DataRow("top secret\\..\\..\\..\\Windows")] + [DataRow("top secret.jpg\\..\\..\\..\\Windows"), DataRow("top secret.jpg//..//..//..\\Windows")] + [DataRow("%2e%2e%2f"), DataRow("%2e%2e%5c")] + [DataRow(".\\%2e%2e%2f"), DataRow("../%2e%2e%2f")] + [DataRow("~a.txt"), DataRow("~a.txt")] + [DataRow("!a.txt"), DataRow("!a.txt")] + public void IsFilePathWhitelistedTest_DenyOnStrict(string path) + { + var defaultScope = new DPFileScopeSettings(new string[] { GetPath("~My Private Info/OMG/Plz No/a.txt"), GetPath("!a.txt" )}, new string[] { GetPath("~") }, true, true); + path = GetPath(path); + Assert.IsFalse(defaultScope.IsFilePathWhitelisted(path)); + } + [TestMethod] + [DataRow("~My Private Info/OMG/Plz No/b.txt"), DataRow("~My Private Info\\OMG\\Plz No\\b.txt")] + [DataRow(".."), DataRow("../"), DataRow("..\\")] + [DataRow("../../Windows"), DataRow("..\\..\\Windows")] + [DataRow("./Windows/exploit.exe"), DataRow(".\\Windows\\exploit.exe")] + [DataRow(".\\"), DataRow("./"), DataRow(".\\")] + [DataRow(".\\exploit.exe"), DataRow("./exploit.exe"), DataRow(".\\exploit.exe")] + [DataRow("top secret/../../../Windows"), DataRow("top secret\\..\\..\\..\\Windows")] + [DataRow("top secret.jpg\\..\\..\\..\\Windows"), DataRow("top secret.jpg//..//..//..\\Windows")] + [DataRow("%2e%2e%2f"), DataRow("%2e%2e%5c")] + [DataRow(".\\%2e%2e%2f"), DataRow("../%2e%2e%2f")] + [DataRow("~b.txt"), DataRow("~b.txt")] + [DataRow("!b.txt"), DataRow("!b.txt")] + [DataRow("!lavarball.txt"), DataRow("!lavarball.txt")] + public void IsFilePathWhitelistedTest_DenyOnStrictFiles(string path) + { + var defaultScope = new DPFileScopeSettings(new string[] { GetPath("~My Private Info/OMG/Plz No/a.txt"), GetPath("!a.txt" )}, new string[] { GetPath("~") }, false, true); + path = GetPath(path); + Assert.IsFalse(defaultScope.IsFilePathWhitelisted(path)); + } + + [TestMethod] + [DataRow("!My Private Info/OMG/Plz No/a.txt"), DataRow("!My Private Info\\OMG\\Plz No\\a.txt")] + [DataRow("!b.txt")] + [DataRow("!a.txt"), DataRow("!a.txt")] + public void IsFilePathWhitelistedTest_DenyOnNoExplicit(string path) + { + var defaultScope = new DPFileScopeSettings(new[] { GetPath("a.txt"), GetPath("~a.txt") }, new[] { GetPath("~") }, false); + path = GetPath(path); + Assert.IsFalse(defaultScope.IsFilePathWhitelisted(path)); + } + + [TestMethod] + [DataRow("~My Private Info/OMG/Plz No/a.txt"), DataRow("~My Private Info\\OMG\\Plz No\\a.txt")] + [DataRow("~My Private Info/OMG/Plz No/b.txt"), DataRow("~My Private Info\\OMG\\Plz No\\b.txt")] + [DataRow(".."), DataRow("../"), DataRow("..\\")] + [DataRow("../../Windows"), DataRow("..\\..\\Windows")] + public void IsFilePathWhitelistedTest_AcceptOnNoEnforcement(string path) + { + var defaultScope = new DPFileScopeSettings(Array.Empty(), Array.Empty(), noEnforcement: true); + path = GetPath(path); + Assert.IsTrue(defaultScope.IsFilePathWhitelisted(path)); + } + + + [TestMethod] + [DataRow("~My Private Info/OMG/Plz No/a.txt"), DataRow("~My Private Info\\OMG\\Plz No\\a.txt")] + [DataRow("~a.txt"), DataRow("~a.txt")] + [DataRow("a.txt")] + [DataRow("~b.txt"), DataRow("~b.txt")] + public void IsFilePathWhitelistedTest_AcceptOnNoExplicit(string path) + { + var defaultScope = new DPFileScopeSettings(new[] { GetPath("a.txt"), GetPath("~a.txt") }, new[] { GetPath("~") }, false); + path = GetPath(path); + Assert.IsTrue(defaultScope.IsFilePathWhitelisted(path)); + } + + [TestMethod] + [DataRow("~a.txt"), DataRow("~a.txt")] + [DataRow("~b.txt"), DataRow("~b.txt")] + [DataRow("~c"), DataRow("~c")] + public void IsFilePathWhitelistedTest_AcceptOnDefault(string path) + { + var defaultScope = new DPFileScopeSettings(Array.Empty(), new string[] { GetPath("~") }); + path = GetPath(path); + Assert.IsTrue(defaultScope.IsFilePathWhitelisted(path)); + } + + [TestMethod] + [DataRow("~a.txt"), DataRow("~a.txt")] + [DataRow("~b.txt"), DataRow("~b.txt")] + public void IsFilePathWhitelistedTest_AcceptOnStrict(string path) + { + var defaultScope = new DPFileScopeSettings(new string[] { GetPath("~a.txt"), GetPath("~b.txt") }, new string[] { GetPath("~") }, true, true); + path = GetPath(path); + Assert.IsTrue(defaultScope.IsFilePathWhitelisted(path)); + } + + [TestMethod] + [DataRow("~a.txt"), DataRow("~a.txt")] + [DataRow("~b.txt"), DataRow("~b.txt")] + [DataRow("!c.txt"), DataRow("!c.txt")] + + public void IsFilePathWhitelistedTest_AcceptOnStrictFiles(string path) + { + var defaultScope = new DPFileScopeSettings(new string[] { GetPath("~a.txt"), GetPath("~b.txt"), GetPath("!c.txt") }, new string[] { GetPath("~") }, false, true); + path = GetPath(path); + Assert.IsTrue(defaultScope.IsFilePathWhitelisted(path)); + } + [TestMethod] + [DataRow("~a.txt"), DataRow("~a.txt")] + [DataRow("~b.txt"), DataRow("~b.txt")] + [DataRow("!c.txt"), DataRow("!c.txt")] + + public void IsFilePathWhitelistedTest_AcceptsFilesOutOfDirViaDefinedFiles(string path) + { + // this test checks to see if we whitelist files that are outside of the defined directories, but are explicitly whitelisted in the files. + var defaultScope = new DPFileScopeSettings(new string[] { GetPath("~a.txt"), GetPath("~b.txt"), GetPath("!c.txt") }, new string[] { GetPath("~") }); + path = GetPath(path); + Assert.IsTrue(defaultScope.IsFilePathWhitelisted(path)); + } + [TestMethod] + public void CreateUltraStrictTest() + { + var scope = DPFileScopeSettings.CreateUltraStrict(new string[] { GetPath("~a.txt") }, new string[] { GetPath("~") }); + Assert.IsFalse(scope.NoEnforcement); + Assert.IsTrue(scope.ThrowOnPathTransversal); + Assert.IsTrue(scope.ExplicitDirectoryPaths); + Assert.IsTrue(scope.ExplicitFilePaths); + } + [TestMethod] + public void NoEnforcementTest() + { + DPFileScopeSettings scope = DPFileScopeSettings.All; + Assert.IsTrue(scope.NoEnforcement); + Assert.IsFalse(scope.ThrowOnPathTransversal); + Assert.IsFalse(scope.ExplicitDirectoryPaths); + Assert.IsFalse(scope.ExplicitFilePaths); + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.IOTests/Fakes/FakeDPDirectoryInfo.cs b/src/DAZ_Installer.IOTests/Fakes/FakeDPDirectoryInfo.cs new file mode 100644 index 0000000..1f93508 --- /dev/null +++ b/src/DAZ_Installer.IOTests/Fakes/FakeDPDirectoryInfo.cs @@ -0,0 +1,32 @@ +namespace DAZ_Installer.IO.Fakes +{ + public class FakeDPDirectoryInfo : IDPDirectoryInfo + { + private readonly DPDirectoryInfo info; + + /// + public FakeDPDirectoryInfo(IDirectoryInfo info, AbstractFileSystem ctx, IDPDirectoryInfo? parent) => this.info = new DPDirectoryInfo(info, ctx, parent); + + public virtual IDPDirectoryInfo? Parent => ((IDPDirectoryInfo)info).Parent; + + public virtual string Name => ((IDPIONode)info).Name; + + public virtual string Path => ((IDPIONode)info).Path; + + public virtual bool Exists => ((IDPIONode)info).Exists; + + public virtual bool Whitelisted => ((IDPIONode)info).Whitelisted; + + public virtual FileAttributes Attributes { get => ((IDPIONode)info).Attributes; set => ((IDPIONode)info).Attributes = value; } + + public virtual AbstractFileSystem FileSystem => ((IDPIONode)info).FileSystem; + + public virtual void Create() => ((IDPDirectoryInfo)info).Create(); + public virtual void Delete(bool recursive) => ((IDPDirectoryInfo)info).Delete(recursive); + public virtual void MoveTo(string path) => ((IDPDirectoryInfo)info).MoveTo(path); + public virtual bool PreviewCreate() => ((IDPDirectoryInfo)info).PreviewCreate(); + public virtual bool PreviewDelete(bool recursive) => ((IDPDirectoryInfo)info).PreviewDelete(recursive); + public virtual bool PreviewMoveTo(string path) => ((IDPDirectoryInfo)info).PreviewMoveTo(path); + public bool TryCreate() => ((IDPDirectoryInfo)info).TryCreate(); + } +} diff --git a/src/DAZ_Installer.IOTests/Fakes/FakeDPDriveInfo.cs b/src/DAZ_Installer.IOTests/Fakes/FakeDPDriveInfo.cs new file mode 100644 index 0000000..6b0af80 --- /dev/null +++ b/src/DAZ_Installer.IOTests/Fakes/FakeDPDriveInfo.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DAZ_Installer.IO.Fakes +{ + [Obsolete("For testing purposes only")] + public class FakeDPDriveInfo : IDPDriveInfo + { + public FakeFileSystem FileSystem { get; init; } + + public virtual long AvailableFreeSpace { get; set; } = long.MaxValue; + public virtual IDPDirectoryInfo RootDirectory { get; set; } = null!; + + public FakeDPDriveInfo(FakeFileSystem fileSystem, string rootDirName) => (FileSystem, RootDirectory) = (fileSystem, fileSystem.CreateDirectoryInfo(rootDirName)); + } +} diff --git a/src/DAZ_Installer.IOTests/Fakes/FakeDPFileInfo.cs b/src/DAZ_Installer.IOTests/Fakes/FakeDPFileInfo.cs new file mode 100644 index 0000000..46d4500 --- /dev/null +++ b/src/DAZ_Installer.IOTests/Fakes/FakeDPFileInfo.cs @@ -0,0 +1,52 @@ +namespace DAZ_Installer.IO.Fakes +{ + [Obsolete("For testing purposes only")] + public class FakeDPFileInfo : IDPFileInfo + { + private readonly DPFileInfo fileInfo; + /// + public FakeDPFileInfo(IFileInfo info, FakeFileSystem fs, IDPDirectoryInfo? directory) => fileInfo = new DPFileInfo(info, fs, directory); + /// + public FakeDPFileInfo(IFileInfo info, AbstractFileSystem fs, IDPDirectoryInfo? directory) => fileInfo = new DPFileInfo(info, fs, directory); + + + public virtual IDPDirectoryInfo? Directory => ((IDPFileInfo)fileInfo).Directory; + + public virtual string Name => ((IDPIONode)fileInfo).Name; + + public virtual string Path => ((IDPIONode)fileInfo).Path; + + public virtual bool Exists => ((IDPIONode)fileInfo).Exists; + + public virtual bool Whitelisted => ((IDPIONode)fileInfo).Whitelisted; + + public virtual FileAttributes Attributes { get => ((IDPIONode)fileInfo).Attributes; set => ((IDPIONode)fileInfo).Attributes = value; } + + public virtual AbstractFileSystem FileSystem => ((IDPIONode)fileInfo).FileSystem; + + public virtual IDPFileInfo CopyTo(string path, bool overwrite) => ((IDPFileInfo)fileInfo).CopyTo(path, overwrite); + public virtual Stream Create() => ((IDPFileInfo)fileInfo).Create(); + public virtual void Delete() => ((IDPFileInfo)fileInfo).Delete(); + public virtual void MoveTo(string path, bool overwrite) => ((IDPFileInfo)fileInfo).MoveTo(path, overwrite); + public virtual Stream Open(FileMode mode, FileAccess access) => ((IDPFileInfo)fileInfo).Open(mode, access); + public virtual Stream OpenRead() => ((IDPFileInfo)fileInfo).OpenRead(); + public virtual Stream OpenWrite() => ((IDPFileInfo)fileInfo).OpenWrite(); + public virtual bool PreviewCopyTo(string path, bool overwrite) => ((IDPFileInfo)fileInfo).PreviewCopyTo(path, overwrite); + public virtual bool PreviewCreate() => ((IDPFileInfo)fileInfo).PreviewCreate(); + public virtual bool PreviewDelete() => ((IDPFileInfo)fileInfo).PreviewDelete(); + public virtual bool PreviewMoveTo(string path, bool overwrite) => ((IDPFileInfo)fileInfo).PreviewMoveTo(path, overwrite); + public virtual bool PreviewOpen(FileMode mode, FileAccess access) => ((IDPFileInfo)fileInfo).PreviewOpen(mode, access); + public virtual bool TryAndFixCopyTo(string path, bool overwrite, out IDPFileInfo? info, out Exception? ex) => ((IDPFileInfo)fileInfo).TryAndFixCopyTo(path, overwrite, out info, out ex); + public virtual bool TryAndFixDelete(out Exception? ex) => ((IDPFileInfo)fileInfo).TryAndFixDelete(out ex); + public virtual bool TryAndFixMoveTo(string path, bool overwrite, out Exception? ex) => ((IDPFileInfo)fileInfo).TryAndFixMoveTo(path, overwrite, out ex); + public virtual bool TryAndFixOpen(FileMode mode, FileAccess access, out Stream? stream, out Exception? ex) => ((IDPFileInfo)fileInfo).TryAndFixOpen(mode, access, out stream, out ex); + public virtual bool TryAndFixOpenRead(out Stream? stream, out Exception? ex) => ((IDPFileInfo)fileInfo).TryAndFixOpenRead(out stream, out ex); + public virtual bool TryAndFixOpenWrite(out Stream? stream, out Exception? ex) => ((IDPFileInfo)fileInfo).TryAndFixOpenWrite(out stream, out ex); + public virtual bool TryCopyTo(string path, bool overwrite, out IDPFileInfo? info) => ((IDPFileInfo)fileInfo).TryCopyTo(path, overwrite, out info); + public virtual bool TryDelete() => ((IDPFileInfo)fileInfo).TryDelete(); + public virtual bool TryMoveTo(string path, bool overwrite) => ((IDPFileInfo)fileInfo).TryMoveTo(path, overwrite); + public virtual bool TryOpen(FileMode mode, FileAccess access, out Stream? stream) => ((IDPFileInfo)fileInfo).TryOpen(mode, access, out stream); + public virtual bool TryOpenRead(out Stream? stream) => ((IDPFileInfo)fileInfo).TryOpenRead(out stream); + public virtual bool TryOpenWrite(out Stream? stream) => ((IDPFileInfo)fileInfo).TryOpenWrite(out stream); + } +} diff --git a/src/DAZ_Installer.IOTests/Fakes/FakeDirectoryInfo.cs b/src/DAZ_Installer.IOTests/Fakes/FakeDirectoryInfo.cs new file mode 100644 index 0000000..295ff16 --- /dev/null +++ b/src/DAZ_Installer.IOTests/Fakes/FakeDirectoryInfo.cs @@ -0,0 +1,69 @@ +using DAZ_Installer.IO; + +namespace DAZ_Installer.IO.Fakes +{ + /// + /// A default fake file info that can be used for testing. All methods and properties are virtual. Never use this code for production. + /// + [Obsolete("This class is only for testing purposes. Do not use in production code.")] + public class FakeDirectoryInfo : IDirectoryInfo + { + /// + /// The name of this directory. Equivalent to . + /// + public virtual string Name => Path.GetFileName(FullName); + /// + /// The full path of the directory. No modifications are made, it is strictly the path provided to the constructor. + /// + public virtual string FullName { get; set; } = string.Empty; + /// + /// Whether the directory exists or not. Defaults to true. + /// + public virtual bool Exists { get; set; } = true; + /// + /// The attributes of the directory. + /// + public virtual FileAttributes Attributes { get; set; } = FileAttributes.Normal; + /// + /// The parent directory of this directory. Defaults to null. + /// + public virtual IDirectoryInfo? Parent { get; set; } = null; + /// + /// Does nothing. + /// + public virtual void Create() { } + /// + /// Sets to false. + /// + public virtual void Delete(bool recursive) => Exists = false; + /// + /// Returns s from (default is empty)."/> + /// + /// + public virtual IEnumerable EnumerateDirectories() => Directories; + /// + public virtual IEnumerable EnumerateDirectories(string _, EnumerationOptions __) => Directories; + /// + /// Returns s from (default is empty)."/> + /// + /// + public virtual IEnumerable EnumerateFiles() => Files; + /// + public virtual IEnumerable EnumerateFiles(string _, EnumerationOptions __) => Files; + /// + /// Directories of this directory; default is empty. + /// + public virtual IEnumerable Directories { get; set; } = Enumerable.Empty(); + /// + /// Files of this directory; default is empty. + /// + public virtual IEnumerable Files { get; set; } = Enumerable.Empty(); + /// + /// Sets to the provided path. + /// + public virtual void MoveTo(string path) => FullName = path; + + public FakeDirectoryInfo(string path) => FullName = path; + public FakeDirectoryInfo(string path, IDirectoryInfo? parent) : this(path) => Parent = parent; + } +} diff --git a/src/DAZ_Installer.IOTests/Fakes/FakeFileInfo.cs b/src/DAZ_Installer.IOTests/Fakes/FakeFileInfo.cs new file mode 100644 index 0000000..8a5db35 --- /dev/null +++ b/src/DAZ_Installer.IOTests/Fakes/FakeFileInfo.cs @@ -0,0 +1,62 @@ +using DAZ_Installer.IO; + +namespace DAZ_Installer.IO.Fakes +{ + /// + /// A default fake file info that can be used for testing. Never use this code for production. + /// + [Obsolete("This class is only for testing purposes. Do not use in production code.")] + public class FakeFileInfo : IFileInfo + { + /// + /// The file name of this file. Equivalent to . + /// + public virtual string Name => Path.GetFileName(FullName); + /// + /// The full path of the file. No modifications are made, it is strictly the path provided to the constructor. + /// + public virtual string FullName { get; set; } = string.Empty; + /// + /// Whether the file exists or not. Defaults to true. + /// + public virtual bool Exists { get; set; } = true; + /// + /// The parent directory of this file. Defaults to null. + /// + public virtual IDirectoryInfo? Directory { get; set; } = null; + /// + /// The full path of the parent directory of this file. Equivalent to . + /// + public virtual string? DirectoryName => Directory?.FullName ?? null; + /// + /// The attributes of this file. Defaults to . + /// + public virtual FileAttributes Attributes { get; set; } = FileAttributes.Normal; + /// + /// CopyTo accepts any path and returns a new with the path provided. + /// + /// A new . + public virtual IFileInfo CopyTo(string path, bool overwrite) => new FakeFileInfo(path); + /// + /// Accepts all parameters and returns . + /// + /// + public virtual Stream Create() => Stream.Null; + /// + /// Sets to false. + /// + public virtual void Delete() => Exists = false; + /// + /// Sets to the provided path. + /// + public virtual void MoveTo(string path, bool overwrite) => FullName = path; + /// + /// Accepts all parameters and returns . + /// + /// + public virtual Stream Open(FileMode mode, FileAccess access) => Stream.Null; + + public FakeFileInfo(string path) => FullName = path; + public FakeFileInfo(string path, IDirectoryInfo? directory) : this(path) => Directory = directory; + } +} diff --git a/src/DAZ_Installer.IOTests/Fakes/FakeFileSystem.cs b/src/DAZ_Installer.IOTests/Fakes/FakeFileSystem.cs new file mode 100644 index 0000000..9da026c --- /dev/null +++ b/src/DAZ_Installer.IOTests/Fakes/FakeFileSystem.cs @@ -0,0 +1,25 @@ +using Moq; +namespace DAZ_Installer.IO.Fakes +{ + [Obsolete("For testing purposes only")] + public class FakeFileSystem : AbstractFileSystem + { + public bool PartialMock = true; + /// + public FakeFileSystem() { } + /// + public FakeFileSystem(DPFileScopeSettings scope) : base(scope) { } + + public override FakeDPDirectoryInfo CreateDirectoryInfo(string path) => new Mock(new Mock(path) { CallBase = PartialMock }.Object, this, null) { CallBase = PartialMock }.Object; + public override FakeDPFileInfo CreateFileInfo(string path) => new Mock(new Mock(path) { CallBase = PartialMock }.Object, this, null) { CallBase = PartialMock }.Object; + public override FakeDPDriveInfo CreateDriveInfo(string path) => new Mock(this, Path.GetPathRoot(path)!) { CallBase = PartialMock }.Object; + public override void DeleteDirectory(string path, bool recursive = false) => throw new NotImplementedException(); + public override void DeleteFile(string path) => throw new NotImplementedException(); + public override IEnumerable EnumerateDirectories(string path) => throw new NotImplementedException(); + public override IEnumerable EnumerateFiles(string path) => throw new NotImplementedException(); + public override bool Exists(string? path, bool treatAsDirectory = false) => throw new NotImplementedException(); + public override IDPDriveInfo[] GetDrives() => new[] { new FakeDPDriveInfo(this, "A:/") }; + protected internal override IDPDirectoryInfo CreateDirectoryInfo(string path, IDPDirectoryInfo? parent) => CreateDirectoryInfo(path, parent); + protected internal override IDPFileInfo CreateFileInfo(string path, IDPDirectoryInfo? directory = null) => CreateFileInfo(path, directory); + } +} diff --git a/src/DAZ_Installer.IOTests/PathHelperTests.cs b/src/DAZ_Installer.IOTests/PathHelperTests.cs new file mode 100644 index 0000000..c3da035 --- /dev/null +++ b/src/DAZ_Installer.IOTests/PathHelperTests.cs @@ -0,0 +1,153 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DAZ_Installer.IO.Tests +{ + [TestClass] + public class PathHelperTests + { + [DataTestMethod] + [DataRow("", "", "")] + [DataRow("Contents\\Documents\\sollybean", "Contents", "Contents/Documents/sollybean")] + [DataRow("C:/Contents/Documents/sollybean", "C:/Contents", "Contents/Documents/sollybean")] + [DataRow("C:/Contents/Documents/sollybean.txt", "C:/Contents", "Contents/Documents/sollybean.txt")] + [DataRow("Content/Documents/Roblox", "Content", "Content/Documents/Roblox")] + [DataRow("My Library/data/DubNation/Golden State Warriors/suck.dsf", "My Library/data", "data/DubNation/Golden State Warriors/suck.dsf")] + [DataRow("C:/My Library/data/2015/Cavaliers/winners.dsf", "C:/My Library", "My Library/data/2015/Cavaliers/winners.dsf")] + [DataRow("Content\\data\\john.dsf", "", "Content\\data\\john.dsf")] + [DataRow("Content\\data\\john.dsf", "Content/data", "data/john.dsf")] + + + + public void GetRelativePathOfRelativeParentTest(string path, string relativeTo, string want) => Assert.AreEqual(want, PathHelper.GetRelativePathOfRelativeParent(path, relativeTo)); + + [DataTestMethod] + [DataRow("C:\\", '\\')] + [DataRow("", '/')] + [DataRow("Contents/Documents/sollybean", '/')] + [DataRow("Contents/Documents\\sollybean", '/')] + + public void GetSeperatorTest(string path, char want) => Assert.AreEqual(want, PathHelper.GetSeperator(path)); + + [DataTestMethod] + [DataRow("C:/", "")] + [DataRow("C:/Contents/Documents/sollybean", "Documents")] + [DataRow("C:/Contents/Documents/sollybean/", "Documents")] + [DataRow("C:\\John\\a.png", "John")] + [DataRow("a.png", "")] + [DataRow("My Library/a.png", "My Library")] + public void GetLastDirTest(string path, string want) + { + var file = path.EndsWith(".png"); + Assert.AreEqual(want, PathHelper.GetLastDir(path, file)); + } + + [DataTestMethod] + [DataRow("Contents", "")] + [DataRow("C:/Contents/Documents/sollybean", "C:/Contents/Documents")] + [DataRow("Contents/Documents/sollybean", "Contents/Documents")] + [DataRow("Contents/Documents", "Contents")] + + public void GetParentTest(string path, string want) => Assert.AreEqual(want, PathHelper.GetParent(path)); + + [DataTestMethod] + [DataRow("C:/Contents/Documents/sollybean", "sollybean")] + [DataRow("C:/Contents/Documents/sollybean/", "")] + [DataRow("C:/Contents/Documents/sollybean.", "sollybean.")] + [DataRow("C:/Contents/Documents/sollybean.txt", "sollybean.txt")] + [DataRow("sollybean.txt", "sollybean.txt")] + [DataRow("sollybean", "sollybean")] + [DataRow("sollybean/", "")] + [DataRow("C:\\", "")] + [DataRow("C:\\Dog/food/a.jpg", "a.jpg")] + [DataRow("", "")] + public void GetFileNameTest(string path, string want) => Assert.AreEqual(want, PathHelper.GetFileName(path)); + + [DataTestMethod] + [DataRow("", "")] + [DataRow("C:/Contents/Documents/sollybean", "C:/Contents/Documents/sollybean")] + [DataRow("C:/Contents/Documents/sollybean/", "C:/Contents/Documents/sollybean")] + [DataRow("Contents/Documents/sollybean", "Contents/Documents/sollybean")] + [DataRow("Contents/Documents/sollybean/", "Contents/Documents/sollybean")] + [DataRow("Documents", "Documents")] + [DataRow("Documents/", "Documents")] + + public void CleanDirPathTest(string path, string want) => Assert.AreEqual(want, PathHelper.CleanDirPath(path)); + + [DataTestMethod] + [DataRow("", "", 0)] + [DataRow("C:/Contents/Documents/sollybean", "C:/Contents/Documents/sollybean", 0)] + [DataRow("Documents", "Documents", 0)] + [DataRow("Content/Documents/Roblox", "Content", 2)] + public void GetNumOfLevelsAboveTest(string path, string relativeTo, int want) => Assert.AreEqual(want, PathHelper.GetNumOfLevelsAbove(path, relativeTo)); + + [DataTestMethod] + [DataRow("", 0)] + [DataRow("C:/Contents/Documents/sollybean", 3)] + [DataRow("Documents", 0)] + [DataRow("Documents/", 0)] + [DataRow("Content/Documents/Roblox", 2)] + [DataRow("Content/Documents/Roblox", 2)] + public void GetSubfoldersCountTest(string path, int want) => Assert.AreEqual(want, PathHelper.GetSubfoldersCount(path)); + + [DataTestMethod] + [DataRow("", "")] + [DataRow("Documents", "Documents")] + [DataRow("Documents\\", "Documents/")] + [DataRow("Documents\\dom.txt", "Documents/dom.txt")] + [DataRow("C:\\Documents\\dom.txt", "C:/Documents/dom.txt")] + [DataRow("C:\\Documents/dom.txt", "C:\\Documents\\dom.txt")] + [DataRow("C:/Documents/dom.txt", "C:\\Documents\\dom.txt")] + public void SwitchSeperatorsTest(string path, string want) => Assert.AreEqual(want, PathHelper.SwitchSeperators(path)); + + [DataTestMethod] + [DataRow("", "")] + [DataRow("Documents", "Documents")] + [DataRow("Documents/text/", "Documents/text")] + [DataRow("Documents/text/a.png", "Documents/text/a.png")] + public void GetDirectoryPathTest(string path, string want) => Assert.AreEqual(want, PathHelper.GetDirectoryPath(path)); + + [DataTestMethod] + [DataRow("", "")] + [DataRow("Documents", "Documents")] + [DataRow("Documents\\", "Documents")] + [DataRow("Documents\\dom.txt", "Documents/dom.txt")] + [DataRow("C:\\Documents\\dom.txt", "C:/Documents/dom.txt")] + [DataRow("C:\\Documents/dom.txt", "C:/Documents/dom.txt")] + [DataRow("/\\/\\/\\", "")] + public void NormalizePathTest(string path, string want) => Assert.AreEqual(want, PathHelper.NormalizePath(path)); + + [DataTestMethod] + [DataRow("", "")] + [DataRow("Documents", "")] + [DataRow("Documents/Roblox", "Documents")] + [DataRow("Documents/Roblox\\People", "Documents/Roblox")] + [DataRow("data\\DAZ 3D", "data")] + [DataRow("data\\DAZ 3D\\Genesis 8", "data\\DAZ 3D")] + [DataRow("data/DAZ 3D/Genesis 8/Male/Morphs/Lexa Kiness/Tidazo/LK Tidazo Body.dsf", "data/DAZ 3D/Genesis 8/Male/Morphs/Lexa Kiness/Tidazo")] + + [DataRow("My Library/data/TheRealSolly/The Little League's Court", "My Library/data/TheRealSolly")] + + public void UpTest(string path, string want) => Assert.AreEqual(want, PathHelper.Up(path)); + + [DataTestMethod] + [DataRow("", false)] + [DataRow("a.png", false)] + [DataRow("invalid_but_not_transversal..", false)] + [DataRow(".............................", false)] + [DataRow("C:/a.png", false), DataRow("C:\\a.png", false)] + [DataRow("C:\\Windows\\a.valid..name", false), DataRow("C:/Windows/a.valid..name", false)] + [DataRow("C:\\Windows\\a.valid_-'..name", false), DataRow("C:/Windows/a.valid_-..name", false)] + [DataRow("C:\\Windows\\a.valid_-'..-_name", false), DataRow("C:/Windows/a.valid_-_..-_name", false)] + [DataRow("C:\\_invalid_but_not_transversal..", false), DataRow("C:/a_invalid_but_not_transversal..", false)] + [DataRow("..", true)] + [DataRow(".", true)] + [DataRow("..\\", true), DataRow("../", true)] + [DataRow("C:\\..\\a.valid..name", true), DataRow("C:/../a.valid..name", true)] + [DataRow("C:\\.\\a.valid_-'..name", true), DataRow("C:/./a.valid_-..name", true)] + [DataRow("abc\\..", true), DataRow("abc/..", true)] + [DataRow("abc\\.", true), DataRow("abc/.", true)] + + + public void CheckForTranversalTest(string path, bool want) => Assert.AreEqual(want, PathHelper.CheckForTranversal(path)); + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.IOTests/PathTransversalExceptionTests.cs b/src/DAZ_Installer.IOTests/PathTransversalExceptionTests.cs new file mode 100644 index 0000000..3bd23d2 --- /dev/null +++ b/src/DAZ_Installer.IOTests/PathTransversalExceptionTests.cs @@ -0,0 +1,45 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +namespace DAZ_Installer.IO.Tests +{ + [TestClass] + public class PathTransversalExceptionTests + { + [DataTestMethod] + [DataRow("..\\")] + + public void ThrowIfTransversalDetectedTest_Throws(string path) => Assert.ThrowsException(() => PathTransversalException.ThrowIfTransversalDetected(path)); + + [DataTestMethod] + [DataRow("C:\\Windows\\System32\\a.weird.directory\\a.jpg")] + public void ThrowIfTransversalDetectedTest_NoThrow(string path) => PathTransversalException.ThrowIfTransversalDetected(path); + + [DataTestMethod] + [DataRow("..\\")] + public void ThrowIfTransversalDetectedTest_ThrowsNullMsg(string path) + { + try + { + PathTransversalException.ThrowIfTransversalDetected(path, null); + } + catch (PathTransversalException ex) + { + Assert.AreEqual($"Path tranversal detected for {path}", ex.Message); + } + } + + [DataTestMethod] + [DataRow("..\\", "i am leg")] + public void ThrowIfTransversalDetectedTest_ThrowsMsg(string path, string msg) + { + try + { + PathTransversalException.ThrowIfTransversalDetected(path, msg); + } + catch (PathTransversalException ex) + { + Assert.AreEqual(msg, ex.Message); + } + } + + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.TestingSuiteWindows/DAZ_Installer.TestingSuiteWindows.csproj b/src/DAZ_Installer.TestingSuiteWindows/DAZ_Installer.TestingSuiteWindows.csproj new file mode 100644 index 0000000..8b6dcfd --- /dev/null +++ b/src/DAZ_Installer.TestingSuiteWindows/DAZ_Installer.TestingSuiteWindows.csproj @@ -0,0 +1,31 @@ + + + + WinExe + net6.0-windows + enable + true + enable + true + x64 + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/DAZ_Installer.TestingSuiteWindows/DPDestinationDeterminerEx.cs b/src/DAZ_Installer.TestingSuiteWindows/DPDestinationDeterminerEx.cs new file mode 100644 index 0000000..0db9d5e --- /dev/null +++ b/src/DAZ_Installer.TestingSuiteWindows/DPDestinationDeterminerEx.cs @@ -0,0 +1,93 @@ +using DAZ_Installer.Core; +using DAZ_Installer.Core.Extraction; +using Serilog; +using Serilog.Context; +using System; +using System.Collections.Generic; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DAZ_Installer.TestingSuiteWindows +{ + /// + /// A destination determiner that uses to determine the destination of each file in the archive. + /// The only difference here is that it extracts the meta files (such as manifest files) to temp before calling the base method. + /// + internal class DPDestinationDeterminerEx : DPDestinationDeterminer + { + private new ILogger Logger { get; } = Log.ForContext(); + public DPDestinationDeterminerEx() : base() { } + public override HashSet DetermineDestinations(DPArchive arc, DPProcessSettings settings) + { + // First, we need to extract the meta files to temp. + // In DPDestinationDeterminer, it does NOT extract any files. If the file is not on disk, it will not read Manifest.dsx files. + // So, we need to extract the meta files to temp. + ReadMetaFiles(arc, ref settings); + + // Now, we can call the base method. + return base.DetermineDestinations(arc, settings); + } + + /// + /// Reads the files listed in . + /// + private void ReadMetaFiles(DPArchive arc, ref DPProcessSettings settings) + { + // Extract the DAZ Files that have not been extracted. + var extractSettings = new DPExtractSettings(settings.TempPath, + arc!.DSXFiles.Where((f) => f.FileInfo is null || !f.FileInfo.Exists), + true, arc); + arc.ExtractContentsToTemp(extractSettings); + Stream? stream = null!; + foreach (DPDSXFile file in arc!.DSXFiles) + { + using (LogContext.PushProperty("File", file.Path)) + // If it did not extract correctly we don't have acces, just skip it. + if (file.FileInfo is null || !file.FileInfo.Exists) + { + Logger.Warning("FileInfo was null or returned does not exist, skipping file to read meta data", file.Path); + Logger.Debug("FileInfo is null: {0}, FileInfo exists: {1}", file.FileInfo is null, file?.FileInfo?.Exists); + continue; + } + try + { + if (!file.FileInfo!.TryAndFixOpenRead(out stream, out Exception? ex)) + { + Logger.Error(ex, "Failed to open read stream for file for reading meta"); + continue; + } + if (stream is null) + { + Logger.Error("OpenRead returned successful but also returned null stream, skipping meta read"); + continue; + } + if (stream.ReadByte() == 0x1F && stream.ReadByte() == 0x8B) + { + // It is gzipped compressed. + stream.Seek(0, SeekOrigin.Begin); + using var gstream = new GZipStream(stream, CompressionMode.Decompress); + using var streamReader = new StreamReader(gstream, Encoding.UTF8, true); + file.CheckContents(streamReader); + } + else + { + // It is normal text. + stream.Seek(0, SeekOrigin.Begin); + using var streamReader = new StreamReader(stream, Encoding.UTF8, true); + file.CheckContents(streamReader); + } + } + catch (Exception ex) + { + Logger.Error(ex, $"Unable to read contents of {file.Path}"); + } + finally + { + stream?.Dispose(); + } + } + } + } +} diff --git a/src/DAZ_Installer.TestingSuiteWindows/MainForm.Designer.cs b/src/DAZ_Installer.TestingSuiteWindows/MainForm.Designer.cs new file mode 100644 index 0000000..ee03589 --- /dev/null +++ b/src/DAZ_Installer.TestingSuiteWindows/MainForm.Designer.cs @@ -0,0 +1,539 @@ +namespace DAZ_Installer.TestingSuiteWindows +{ + partial class MainForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + archiveToTestLbl = new Label(); + archiveTxtBox = new TextBox(); + browseArchiveBtn = new Button(); + openFileDialog1 = new OpenFileDialog(); + folderBrowserDialog1 = new FolderBrowserDialog(); + destPathTxtBox = new TextBox(); + browseDestBtn = new Button(); + treeView1 = new TreeView(); + treeViewMnuStrip = new ContextMenuStrip(components); + copyNameToolStripMenuItem = new ToolStripMenuItem(); + copyFullPathToolStripMenuItem = new ToolStripMenuItem(); + markAsShouldProcessToolStripMenuItem = new ToolStripMenuItem(); + markAsShouldntProcessToolStripMenuItem = new ToolStripMenuItem(); + logOutputTxtBox = new RichTextBox(); + processBtn = new Button(); + toolTip1 = new ToolTip(components); + destPathLbl = new Label(); + tempLbl = new Label(); + tempPathTxtBox = new TextBox(); + browseTempBtn = new Button(); + changeProcessBtn = new Button(); + peekBtn = new Button(); + determineBtn = new Button(); + extractBtn = new Button(); + peekRecursivelyBtn = new Button(); + tabControl1 = new TabControl(); + logsTab = new TabPage(); + filesExtractedTab = new TabPage(); + processedTxtBox = new RichTextBox(); + extractRecursivelyBtn = new Button(); + cancelBtn = new Button(); + deleteFilesChkBox = new CheckBox(); + clearLogsChkBox = new CheckBox(); + disableWarningChkBox = new CheckBox(); + saveBtn = new Button(); + label1 = new Label(); + saveBrowseBtn = new Button(); + saveTxtBox = new TextBox(); + autoSaveBtn = new CheckBox(); + progressBar1 = new ProgressBar(); + treeViewMnuStrip.SuspendLayout(); + tabControl1.SuspendLayout(); + logsTab.SuspendLayout(); + filesExtractedTab.SuspendLayout(); + SuspendLayout(); + // + // archiveToTestLbl + // + archiveToTestLbl.AutoSize = true; + archiveToTestLbl.Location = new Point(12, 9); + archiveToTestLbl.Name = "archiveToTestLbl"; + archiveToTestLbl.Size = new Size(152, 15); + archiveToTestLbl.TabIndex = 0; + archiveToTestLbl.Text = "Archive to test (rar, 7z, zip): "; + // + // archiveTxtBox + // + archiveTxtBox.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + archiveTxtBox.Location = new Point(12, 27); + archiveTxtBox.Name = "archiveTxtBox"; + archiveTxtBox.Size = new Size(431, 23); + archiveTxtBox.TabIndex = 1; + // + // browseArchiveBtn + // + browseArchiveBtn.Anchor = AnchorStyles.Top | AnchorStyles.Right; + browseArchiveBtn.Location = new Point(449, 27); + browseArchiveBtn.Name = "browseArchiveBtn"; + browseArchiveBtn.Size = new Size(31, 23); + browseArchiveBtn.TabIndex = 2; + browseArchiveBtn.Text = "..."; + browseArchiveBtn.UseVisualStyleBackColor = true; + browseArchiveBtn.Click += browseArchiveBtn_Click; + // + // openFileDialog1 + // + openFileDialog1.FileName = "openFileDialog1"; + // + // folderBrowserDialog1 + // + folderBrowserDialog1.UseDescriptionForTitle = true; + // + // destPathTxtBox + // + destPathTxtBox.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + destPathTxtBox.Location = new Point(115, 58); + destPathTxtBox.Name = "destPathTxtBox"; + destPathTxtBox.Size = new Size(328, 23); + destPathTxtBox.TabIndex = 6; + // + // browseDestBtn + // + browseDestBtn.Anchor = AnchorStyles.Top | AnchorStyles.Right; + browseDestBtn.Location = new Point(449, 59); + browseDestBtn.Name = "browseDestBtn"; + browseDestBtn.Size = new Size(31, 23); + browseDestBtn.TabIndex = 7; + browseDestBtn.Text = "..."; + browseDestBtn.UseVisualStyleBackColor = true; + browseDestBtn.Click += browseDestBtn_Click; + // + // treeView1 + // + treeView1.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + treeView1.ContextMenuStrip = treeViewMnuStrip; + treeView1.Location = new Point(13, 169); + treeView1.Name = "treeView1"; + treeView1.Size = new Size(468, 318); + treeView1.TabIndex = 8; + // + // treeViewMnuStrip + // + treeViewMnuStrip.Items.AddRange(new ToolStripItem[] { copyNameToolStripMenuItem, copyFullPathToolStripMenuItem, markAsShouldProcessToolStripMenuItem, markAsShouldntProcessToolStripMenuItem }); + treeViewMnuStrip.Name = "treeViewMnuStrip"; + treeViewMnuStrip.Size = new Size(212, 92); + treeViewMnuStrip.Opening += treeViewMnuStrip_Opening; + // + // copyNameToolStripMenuItem + // + copyNameToolStripMenuItem.Name = "copyNameToolStripMenuItem"; + copyNameToolStripMenuItem.Size = new Size(211, 22); + copyNameToolStripMenuItem.Text = "Copy name"; + copyNameToolStripMenuItem.Click += copyNameToolStripMenuItem_Click; + // + // copyFullPathToolStripMenuItem + // + copyFullPathToolStripMenuItem.Name = "copyFullPathToolStripMenuItem"; + copyFullPathToolStripMenuItem.Size = new Size(211, 22); + copyFullPathToolStripMenuItem.Text = "Copy full path"; + copyFullPathToolStripMenuItem.Click += copyFullPathToolStripMenuItem_Click; + // + // markAsShouldProcessToolStripMenuItem + // + markAsShouldProcessToolStripMenuItem.Enabled = false; + markAsShouldProcessToolStripMenuItem.Name = "markAsShouldProcessToolStripMenuItem"; + markAsShouldProcessToolStripMenuItem.Size = new Size(211, 22); + markAsShouldProcessToolStripMenuItem.Text = "Mark as should process"; + markAsShouldProcessToolStripMenuItem.Click += markAsShouldProcessToolStripMenuItem_Click; + // + // markAsShouldntProcessToolStripMenuItem + // + markAsShouldntProcessToolStripMenuItem.Name = "markAsShouldntProcessToolStripMenuItem"; + markAsShouldntProcessToolStripMenuItem.Size = new Size(211, 22); + markAsShouldntProcessToolStripMenuItem.Text = "Mark as shouldn't process"; + markAsShouldntProcessToolStripMenuItem.Click += markAsShouldntProcessToolStripMenuItem_Click; + // + // logOutputTxtBox + // + logOutputTxtBox.Dock = DockStyle.Fill; + logOutputTxtBox.Location = new Point(3, 3); + logOutputTxtBox.Name = "logOutputTxtBox"; + logOutputTxtBox.Size = new Size(454, 123); + logOutputTxtBox.TabIndex = 10; + logOutputTxtBox.Text = ""; + // + // processBtn + // + processBtn.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + processBtn.Location = new Point(382, 689); + processBtn.Name = "processBtn"; + processBtn.Size = new Size(98, 33); + processBtn.TabIndex = 11; + processBtn.Text = "Process Archive"; + processBtn.UseVisualStyleBackColor = true; + processBtn.Click += processBtn_Click; + // + // destPathLbl + // + destPathLbl.AutoSize = true; + destPathLbl.Location = new Point(13, 62); + destPathLbl.Name = "destPathLbl"; + destPathLbl.Size = new Size(97, 15); + destPathLbl.TabIndex = 5; + destPathLbl.Text = "Destination Path:"; + // + // tempLbl + // + tempLbl.AutoSize = true; + tempLbl.Location = new Point(44, 91); + tempLbl.Name = "tempLbl"; + tempLbl.Size = new Size(66, 15); + tempLbl.TabIndex = 12; + tempLbl.Text = "Temp Path:"; + // + // tempPathTxtBox + // + tempPathTxtBox.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + tempPathTxtBox.Location = new Point(115, 88); + tempPathTxtBox.Name = "tempPathTxtBox"; + tempPathTxtBox.Size = new Size(328, 23); + tempPathTxtBox.TabIndex = 13; + // + // browseTempBtn + // + browseTempBtn.Anchor = AnchorStyles.Top | AnchorStyles.Right; + browseTempBtn.Location = new Point(449, 87); + browseTempBtn.Name = "browseTempBtn"; + browseTempBtn.Size = new Size(31, 23); + browseTempBtn.TabIndex = 14; + browseTempBtn.Text = "..."; + browseTempBtn.UseVisualStyleBackColor = true; + browseTempBtn.Click += browseTempBtn_Click; + // + // changeProcessBtn + // + changeProcessBtn.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + changeProcessBtn.Location = new Point(13, 115); + changeProcessBtn.Name = "changeProcessBtn"; + changeProcessBtn.Size = new Size(467, 23); + changeProcessBtn.TabIndex = 15; + changeProcessBtn.Text = "Change Process Settings"; + changeProcessBtn.UseVisualStyleBackColor = true; + changeProcessBtn.Click += changeProcessBtn_Click; + // + // peekBtn + // + peekBtn.Anchor = AnchorStyles.Bottom | AnchorStyles.Left; + peekBtn.Location = new Point(12, 689); + peekBtn.Name = "peekBtn"; + peekBtn.Size = new Size(98, 33); + peekBtn.TabIndex = 16; + peekBtn.Text = "Peek Only"; + peekBtn.UseVisualStyleBackColor = true; + peekBtn.Click += peekBtn_Click; + // + // determineBtn + // + determineBtn.Anchor = AnchorStyles.Bottom | AnchorStyles.Left; + determineBtn.Location = new Point(116, 689); + determineBtn.Name = "determineBtn"; + determineBtn.Size = new Size(155, 33); + determineBtn.TabIndex = 17; + determineBtn.Text = "Determine Destinations"; + determineBtn.UseVisualStyleBackColor = true; + determineBtn.Click += determineBtn_Click; + // + // extractBtn + // + extractBtn.Anchor = AnchorStyles.Bottom | AnchorStyles.Left; + extractBtn.Location = new Point(277, 689); + extractBtn.Name = "extractBtn"; + extractBtn.Size = new Size(99, 33); + extractBtn.TabIndex = 18; + extractBtn.Text = "Extract Only"; + extractBtn.UseVisualStyleBackColor = true; + extractBtn.Click += extractBtn_Click; + // + // peekRecursivelyBtn + // + peekRecursivelyBtn.Anchor = AnchorStyles.Bottom | AnchorStyles.Left; + peekRecursivelyBtn.Location = new Point(13, 728); + peekRecursivelyBtn.Name = "peekRecursivelyBtn"; + peekRecursivelyBtn.Size = new Size(97, 47); + peekRecursivelyBtn.TabIndex = 19; + peekRecursivelyBtn.Text = "Peek Recursively"; + peekRecursivelyBtn.UseVisualStyleBackColor = true; + peekRecursivelyBtn.Click += peekRecursivelyBtn_Click; + // + // tabControl1 + // + tabControl1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + tabControl1.Controls.Add(logsTab); + tabControl1.Controls.Add(filesExtractedTab); + tabControl1.Location = new Point(13, 493); + tabControl1.Name = "tabControl1"; + tabControl1.SelectedIndex = 0; + tabControl1.Size = new Size(468, 157); + tabControl1.TabIndex = 20; + // + // logsTab + // + logsTab.Controls.Add(logOutputTxtBox); + logsTab.Location = new Point(4, 24); + logsTab.Name = "logsTab"; + logsTab.Padding = new Padding(3); + logsTab.Size = new Size(460, 129); + logsTab.TabIndex = 0; + logsTab.Text = "Logs"; + logsTab.UseVisualStyleBackColor = true; + // + // filesExtractedTab + // + filesExtractedTab.Controls.Add(processedTxtBox); + filesExtractedTab.Location = new Point(4, 24); + filesExtractedTab.Name = "filesExtractedTab"; + filesExtractedTab.Size = new Size(460, 129); + filesExtractedTab.TabIndex = 2; + filesExtractedTab.Text = "Processed Files"; + filesExtractedTab.UseVisualStyleBackColor = true; + // + // processedTxtBox + // + processedTxtBox.Dock = DockStyle.Fill; + processedTxtBox.Location = new Point(0, 0); + processedTxtBox.Name = "processedTxtBox"; + processedTxtBox.Size = new Size(460, 129); + processedTxtBox.TabIndex = 0; + processedTxtBox.Text = ""; + // + // extractRecursivelyBtn + // + extractRecursivelyBtn.Anchor = AnchorStyles.Bottom | AnchorStyles.Left; + extractRecursivelyBtn.Location = new Point(116, 728); + extractRecursivelyBtn.Name = "extractRecursivelyBtn"; + extractRecursivelyBtn.Size = new Size(155, 47); + extractRecursivelyBtn.TabIndex = 21; + extractRecursivelyBtn.Text = "Extract Recursively"; + extractRecursivelyBtn.UseVisualStyleBackColor = true; + // + // cancelBtn + // + cancelBtn.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + cancelBtn.Enabled = false; + cancelBtn.Location = new Point(382, 728); + cancelBtn.Name = "cancelBtn"; + cancelBtn.Size = new Size(98, 47); + cancelBtn.TabIndex = 22; + cancelBtn.Text = "Cancel"; + cancelBtn.UseVisualStyleBackColor = true; + cancelBtn.Click += cancelBtn_Click; + // + // deleteFilesChkBox + // + deleteFilesChkBox.AutoSize = true; + deleteFilesChkBox.Checked = true; + deleteFilesChkBox.CheckState = CheckState.Checked; + deleteFilesChkBox.Location = new Point(13, 144); + deleteFilesChkBox.Name = "deleteFilesChkBox"; + deleteFilesChkBox.Size = new Size(158, 19); + deleteFilesChkBox.TabIndex = 23; + deleteFilesChkBox.Text = "Delete files automatically"; + deleteFilesChkBox.UseVisualStyleBackColor = true; + // + // clearLogsChkBox + // + clearLogsChkBox.AutoSize = true; + clearLogsChkBox.Checked = true; + clearLogsChkBox.CheckState = CheckState.Checked; + clearLogsChkBox.Location = new Point(184, 144); + clearLogsChkBox.Name = "clearLogsChkBox"; + clearLogsChkBox.Size = new Size(153, 19); + clearLogsChkBox.TabIndex = 24; + clearLogsChkBox.Text = "Clear logs automatically"; + clearLogsChkBox.UseVisualStyleBackColor = true; + // + // disableWarningChkBox + // + disableWarningChkBox.AutoSize = true; + disableWarningChkBox.Location = new Point(349, 144); + disableWarningChkBox.Name = "disableWarningChkBox"; + disableWarningChkBox.Size = new Size(115, 19); + disableWarningChkBox.TabIndex = 25; + disableWarningChkBox.Text = "Disable warnings"; + disableWarningChkBox.UseVisualStyleBackColor = true; + // + // saveBtn + // + saveBtn.Anchor = AnchorStyles.Bottom | AnchorStyles.Left; + saveBtn.Location = new Point(278, 728); + saveBtn.Name = "saveBtn"; + saveBtn.Size = new Size(98, 47); + saveBtn.TabIndex = 26; + saveBtn.Text = "Save output"; + saveBtn.UseVisualStyleBackColor = true; + saveBtn.Click += saveBtn_Click; + // + // label1 + // + label1.Anchor = AnchorStyles.Bottom | AnchorStyles.Left; + label1.AutoSize = true; + label1.Location = new Point(93, 660); + label1.Name = "label1"; + label1.Size = new Size(119, 15); + label1.TabIndex = 27; + label1.Text = "Save output location:"; + // + // saveBrowseBtn + // + saveBrowseBtn.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + saveBrowseBtn.Location = new Point(450, 655); + saveBrowseBtn.Name = "saveBrowseBtn"; + saveBrowseBtn.Size = new Size(31, 23); + saveBrowseBtn.TabIndex = 29; + saveBrowseBtn.Text = "..."; + saveBrowseBtn.UseVisualStyleBackColor = true; + saveBrowseBtn.Click += saveBrowseBtn_Click; + // + // saveTxtBox + // + saveTxtBox.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + saveTxtBox.Location = new Point(213, 655); + saveTxtBox.Name = "saveTxtBox"; + saveTxtBox.Size = new Size(231, 23); + saveTxtBox.TabIndex = 28; + // + // autoSaveBtn + // + autoSaveBtn.Anchor = AnchorStyles.Bottom | AnchorStyles.Left; + autoSaveBtn.AutoSize = true; + autoSaveBtn.Location = new Point(12, 659); + autoSaveBtn.Name = "autoSaveBtn"; + autoSaveBtn.Size = new Size(75, 19); + autoSaveBtn.TabIndex = 30; + autoSaveBtn.Text = "Autosave"; + autoSaveBtn.UseVisualStyleBackColor = true; + // + // progressBar1 + // + progressBar1.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + progressBar1.Location = new Point(12, 781); + progressBar1.MarqueeAnimationSpeed = 10; + progressBar1.Name = "progressBar1"; + progressBar1.Size = new Size(469, 26); + progressBar1.Style = ProgressBarStyle.Continuous; + progressBar1.TabIndex = 31; + // + // MainForm + // + AllowDrop = true; + AutoScaleDimensions = new SizeF(96F, 96F); + AutoScaleMode = AutoScaleMode.Dpi; + ClientSize = new Size(492, 819); + Controls.Add(progressBar1); + Controls.Add(autoSaveBtn); + Controls.Add(saveBrowseBtn); + Controls.Add(saveTxtBox); + Controls.Add(label1); + Controls.Add(saveBtn); + Controls.Add(disableWarningChkBox); + Controls.Add(clearLogsChkBox); + Controls.Add(deleteFilesChkBox); + Controls.Add(cancelBtn); + Controls.Add(extractRecursivelyBtn); + Controls.Add(tabControl1); + Controls.Add(peekRecursivelyBtn); + Controls.Add(extractBtn); + Controls.Add(determineBtn); + Controls.Add(peekBtn); + Controls.Add(changeProcessBtn); + Controls.Add(browseTempBtn); + Controls.Add(tempPathTxtBox); + Controls.Add(tempLbl); + Controls.Add(processBtn); + Controls.Add(treeView1); + Controls.Add(browseDestBtn); + Controls.Add(destPathTxtBox); + Controls.Add(destPathLbl); + Controls.Add(browseArchiveBtn); + Controls.Add(archiveTxtBox); + Controls.Add(archiveToTestLbl); + MinimumSize = new Size(450, 600); + Name = "MainForm"; + Text = "Testing Suite"; + Load += MainForm_Load; + DragDrop += MainForm_DragDrop; + DragEnter += MainForm_DragEnter; + treeViewMnuStrip.ResumeLayout(false); + tabControl1.ResumeLayout(false); + logsTab.ResumeLayout(false); + filesExtractedTab.ResumeLayout(false); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private Label archiveToTestLbl; + private TextBox archiveTxtBox; + private Button browseArchiveBtn; + private OpenFileDialog openFileDialog1; + private FolderBrowserDialog folderBrowserDialog1; + private TextBox destPathTxtBox; + private Button browseDestBtn; + private TreeView treeView1; + private Button processBtn; + private ToolTip toolTip1; + private Label destPathLbl; + private Label tempLbl; + private TextBox tempPathTxtBox; + private Button browseTempBtn; + private Button changeProcessBtn; + private Button peekBtn; + private Button determineBtn; + private Button extractBtn; + private Button peekRecursivelyBtn; + private TabControl tabControl1; + private TabPage logsTab; + private TabPage filesExtractedTab; + private RichTextBox processedTxtBox; + internal RichTextBox logOutputTxtBox; + private Button extractRecursivelyBtn; + private Button cancelBtn; + private CheckBox deleteFilesChkBox; + private CheckBox clearLogsChkBox; + private CheckBox disableWarningChkBox; + private Button saveBtn; + private Label label1; + private Button saveBrowseBtn; + private TextBox saveTxtBox; + private CheckBox autoSaveBtn; + private ContextMenuStrip treeViewMnuStrip; + private ToolStripMenuItem copyNameToolStripMenuItem; + private ToolStripMenuItem copyFullPathToolStripMenuItem; + private ToolStripMenuItem markAsShouldProcessToolStripMenuItem; + private ToolStripMenuItem markAsShouldntProcessToolStripMenuItem; + private ProgressBar progressBar1; + } +} diff --git a/src/DAZ_Installer.TestingSuiteWindows/MainForm.cs b/src/DAZ_Installer.TestingSuiteWindows/MainForm.cs new file mode 100644 index 0000000..d3d834b --- /dev/null +++ b/src/DAZ_Installer.TestingSuiteWindows/MainForm.cs @@ -0,0 +1,682 @@ +using DAZ_Installer.Core; +using DAZ_Installer.IO; +using Serilog; +using System.Text.Json; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using DAZ_Installer.Core.Extraction; +using System.Text; + +namespace DAZ_Installer.TestingSuiteWindows +{ + public partial class MainForm : Form + { + private record class ProcessSession(DPProcessor Processor, DPArchive Archive, DPProcessSettings Settings, List Reports); + + public static MainForm Instance = null!; + DPFileScopeSettings Scope = new DPFileScopeSettings(); + DPProcessSettings settings = new(); + private Task? lastTask; + private DPArchive? lastRootArchive; + ProcessSession? lastSession = null; + DPProcessor? currentProcessor = null; + CancellationTokenSource tokenSource = new(); + public MainForm() + { + Instance = this; + InitializeComponent(); + } + + private void MainForm_Load(object sender, EventArgs e) + { + saveTxtBox.Text = Path.Combine(Environment.CurrentDirectory, "Output"); + settings.TempPath = tempPathTxtBox.Text = Path.Combine(Program.TempPath, "Temp"); + settings.DestinationPath = destPathTxtBox.Text = Path.Combine(Program.TempPath, "Destination"); + settings.ForceFileToDest = new(0); + settings.ContentFolders = new(DPProcessor.DefaultContentFolders); + settings.ContentRedirectFolders = new(DPProcessor.DefaultRedirects); + settings.InstallOption = InstallOptions.ManifestAndAuto; + settings.OverwriteFiles = true; + Scope = new(Enumerable.Empty(), new[] { settings.TempPath, settings.DestinationPath }, false, false, true); + MessageBox.Show("Warning! Do NOT use this application on your main DAZ Studio library. This application is meant for testing purposes only. " + + "Doing so WILL result in your data being lost!\n\nIf you are unsure, leave the settings at default or as instructed by a contributor of this project. " + + "YOU HAVE BEEN WARNED!", + "Warning", + MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + + private void browseArchiveBtn_Click(object sender, EventArgs e) + { + openFileDialog1.Filter = "Acceptable Archives|*.7z;*.rar;*.zip;*.001"; + if (openFileDialog1.ShowDialog() != DialogResult.OK) return; + archiveTxtBox.Text = openFileDialog1.FileName; + } + + private void browseDestBtn_Click(object sender, EventArgs e) + { + folderBrowserDialog1.Description = "Select the folder to install product into"; + if (folderBrowserDialog1.ShowDialog() != DialogResult.OK) return; + destPathTxtBox.Text = folderBrowserDialog1.SelectedPath; + } + + private void browseTempBtn_Click(object sender, EventArgs e) + { + folderBrowserDialog1.Description = "Select the folder to use for temporary usage"; + if (folderBrowserDialog1.ShowDialog() != DialogResult.OK) return; + destPathTxtBox.Text = folderBrowserDialog1.SelectedPath; + } + + private void determineBtn_Click(object sender, EventArgs e) + { + startTask(determineDestinationsTask); + } + + private void extractBtn_Click(object sender, EventArgs e) + { + startTask(extractTask); + } + + private void processBtn_Click(object sender, EventArgs e) + { + startTask(processTask); + } + + private void peekBtn_Click(object sender, EventArgs e) => startTask(peekTask); + private void peekRecursivelyBtn_Click(object sender, EventArgs e) + { + startTask(peekRecursivelyTask); + } + + private void changeProcessBtn_Click(object sender, EventArgs e) + { + var a = new ProcessSettingsDialogue(settings); + a.ShowDialog(); + a.Settings = settings; + } + private void saveBrowseBtn_Click(object sender, EventArgs e) + { + folderBrowserDialog1.Description = "Select the folder to save process output"; + if (folderBrowserDialog1.ShowDialog() != DialogResult.OK) return; + saveTxtBox.Text = folderBrowserDialog1.SelectedPath; + } + + private void saveBtn_Click(object sender, EventArgs e) + { + saveProcess(true); + } + + private void treeViewMnuStrip_Opening(object sender, System.ComponentModel.CancelEventArgs e) + { + if (treeView1.SelectedNode is null) return; + var processed = treeView1.SelectedNode.ForeColor == Color.Green; + markAsShouldntProcessToolStripMenuItem.Enabled = processed; + markAsShouldProcessToolStripMenuItem.Enabled = !processed; + } + + private void copyNameToolStripMenuItem_Click(object sender, EventArgs e) + { + Clipboard.SetText(treeView1.SelectedNode.Text); + } + + private void copyFullPathToolStripMenuItem_Click(object sender, EventArgs e) + { + var node = treeView1.SelectedNode.Tag as DPAbstractNode; + Clipboard.SetText(node!.NormalizedPath); + } + + private void markAsShouldProcessToolStripMenuItem_Click(object sender, EventArgs e) + { + treeView1.BeginUpdate(); + var color = Color.Green; + markNodeAndDescendants(treeView1.SelectedNode, ref color, true); + treeView1.EndUpdate(); + } + + private void markNodeAndDescendants(TreeNode node, ref Color color, bool bold = false) + { + node.ForeColor = color; + if (bold) node.NodeFont = new Font(treeView1.Font, FontStyle.Bold); + else node.NodeFont = new Font(treeView1.Font, FontStyle.Regular); + foreach (var child in node.Nodes.Cast()) + { + markNodeAndDescendants(child, ref color, bold); + } + } + + private void markAsShouldntProcessToolStripMenuItem_Click(object sender, EventArgs e) + { + treeView1.BeginUpdate(); + var color = Color.Black; + markNodeAndDescendants(treeView1.SelectedNode, ref color); + treeView1.EndUpdate(); + } + + private bool validateSettings() + { + settings.DestinationPath = destPathTxtBox.Text; + settings.TempPath = tempPathTxtBox.Text; + var disableWarnings = disableWarningChkBox.Checked; + var savePath = saveTxtBox.Text; + if (!disableWarnings && settings.ContentFolders!.Count == 0) + { + // Turn this into a warning with a prompt. + if (MessageBox.Show("You do not have any content folders, do you wish to continue?", "No content folders", MessageBoxButtons.YesNo, MessageBoxIcon.Error) != DialogResult.Yes) return false; + } + if (!disableWarnings && settings.ContentRedirectFolders!.Count == 0) + { + if (MessageBox.Show("You do not have any content folder aliases, do you wish to continue?", "No content folder aliases", MessageBoxButtons.YesNo, MessageBoxIcon.Error) != DialogResult.Yes) return false; + } + if (settings.ContentRedirectFolders.Any(x => !settings.ContentFolders.Contains(x.Value))) + { + MessageBox.Show("You cannot have a content folder alias that does not point to a valid content folder.", "Invalid content folder alias", MessageBoxButtons.OK, MessageBoxIcon.Error); + return false; + } + if (!Directory.Exists(settings.DestinationPath)) + { + var a = new DPFileSystem(new DPFileScopeSettings(Enumerable.Empty(), new[] { settings.DestinationPath })); + var d = a.CreateDirectoryInfo(settings.DestinationPath); + if (!d.TryCreate()) + { + MessageBox.Show("The destination folder does not exist and could not be created.", "Destination folder does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error); + return false; + } + } + if (!Directory.Exists(settings.TempPath)) + { + var a = new DPFileSystem(new DPFileScopeSettings(Enumerable.Empty(), new[] { settings.TempPath })); + var d = a.CreateDirectoryInfo(settings.TempPath); + if (!d.TryCreate()) + { + MessageBox.Show("The temp folder does not exist and could not be created.", "Temp folder does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error); + return false; + } + } + if (!Directory.Exists(savePath)) + { + var a = new DPFileSystem(new DPFileScopeSettings(Enumerable.Empty(), new[] { savePath })); + var d = a.CreateDirectoryInfo(savePath); + if (!d.TryCreate()) + { + MessageBox.Show("The save output path does not exist and could not be created.", "Temp folder does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error); + return false; + } + } + if (!disableWarnings && Directory.EnumerateFileSystemEntries(settings.DestinationPath).Any()) + { + if (MessageBox.Show("The destination folder is not empty. Are you sure you want to continue?", "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) return false; + } + if (!disableWarnings && Directory.EnumerateFileSystemEntries(settings.TempPath).Any()) + { + if (MessageBox.Show("The temporary folder is not empty. Are you sure you want to continue?", "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) return false; + } + if (!File.Exists(archiveTxtBox.Text)) + { + MessageBox.Show("The archive to test does not exist.", "Archive does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error); + return false; + } + return true; + } + + private DPArchive setupArchive() + { + var archiveLocation = archiveTxtBox.Text; + Scope = new(new[] { archiveLocation }, new[] { settings.TempPath, settings.DestinationPath }, false, false, true); + return new DPArchive(new DPFileSystem(Scope).CreateFileInfo(archiveLocation)); + } + + private bool startTask(Action a) + { + var deleteFiles = deleteFilesChkBox.Checked; + if (!validateSettings()) return false; + if (clearLogsChkBox.Checked) logOutputTxtBox.Clear(); + if (deleteFilesChkBox.Checked) + if (!lastTask?.IsCompleted ?? false) + { + MessageBox.Show("The last task has not completed yet. Please cancel first before continuing.", "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); + return false; + } + lastTask = Task.Run(() => + { + try + { + a(); + } + catch (Exception ex) + { + Log.Error(ex, $"An error occurred while running task: {a.Method.Name}"); + MessageBox.Show($"An error occurred while running a task.\nError message: \n{ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + BeginInvoke(() => updateProgression(false)); + if (deleteFiles) this.deleteFiles(); + }); + updateProgression(true); + return true; + } + + private void updateProgression(bool enable = true) + { + cancelBtn.Enabled = enable; + changeProgressionBar(enable); + } + private void changeProgressionBar(bool marquee = false) + { + progressBar1.Style = marquee ? ProgressBarStyle.Marquee : ProgressBarStyle.Continuous; + progressBar1.Value = marquee ? 0 : 100; + } + + private void deleteFiles() + { + try + { + Directory.Delete(settings.TempPath, true); + Directory.Delete(settings.DestinationPath, true); + } + catch (Exception ex) + { + Log.Error(ex, "An error occurred while deleting temporary files"); + } + } + + private void buildTree(DPArchive arc) + { + var rootNode = new TreeNode(arc.FileName); + Queue queue = new(); + // Add root contents first. + foreach (var file in arc.RootContents) + { + var rootFile = new TreeNode(file.FileName) { Tag = file }; + rootNode.Nodes.Add(rootFile); + } + + // Setup the TreeNodes for the root folders first. + foreach (var rootFolder in arc.RootFolders) + { + var rootFolderNode = new TreeNode(rootFolder.FileName) { Tag = rootFolder }; + rootNode.Nodes.Add(rootFolderNode); + queue.Enqueue(rootFolderNode); + } + + // Now do the rest of the contents starting with the root folders. + while (queue.Count != 0) + { + var parentNode = queue.Dequeue(); + var folder = (DPFolder)parentNode.Tag; + foreach (var file in folder.Contents) + { + var childNode = new TreeNode(file.FileName) { Tag = file }; + parentNode.Nodes.Add(childNode); + } + foreach (var subFolder in folder.subfolders) + { + var childNode = new TreeNode(subFolder.FileName) { Tag = subFolder }; + parentNode.Nodes.Add(childNode); + queue.Enqueue(childNode); + } + } + + // Add it to the tree. + try + { + // If this is the root archive, then we need to pause drawing operations. + if (arc.AssociatedArchive is null) BeginInvoke(treeView1.BeginUpdate); + + // Let the UI thread begin adding the nodes (we are on a thread pool thread). + var result = BeginInvoke(() => treeView1.Nodes.Add(rootNode)); + + // Add the subarchives. + foreach (var subArchive in arc.Subarchives) + { + buildTree(subArchive); + } + + // Wait for the tree to finish updating. + result.AsyncWaitHandle.WaitOne(); + } + finally + { + // We need to resume drawing operations at all costs. + if (arc.AssociatedArchive is null) BeginInvoke(treeView1.EndUpdate); + } + + + } + + private void saveProcess(bool manual = false) + { + if (lastSession is null) return; + var time = DateTime.Now.ToString("d-m-yyyy-hh-mm-ss"); + if (!Directory.Exists(saveTxtBox.Text)) + { + try + { + Directory.CreateDirectory(saveTxtBox.Text); + } + catch + { + MessageBox.Show("Failed to save process output. The directory does not exist and could not be created.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + var path = Path.Combine(saveTxtBox.Text, $"{lastSession.Archive.FileName}_results_{time}.json"); + if (manual) + File.WriteAllTextAsync(path, ResultCompiler.CompileResults(treeView1.Nodes, lastSession.Settings, lastSession.Archive)); + else + File.WriteAllTextAsync(path, ResultCompiler.CompileResults(lastSession.Reports, lastSession.Settings, lastSession.Archive)); + } + + private void colorDetermined(HashSet determinedFiles) + { + List nodes = new(determinedFiles.Count); + // initialize the queue with root-level nodes first; the root-level nodes + // represent all of the archives processed (which can happen if there are multiple archives in the root) + Queue queue = new Queue(treeView1.Nodes.Cast()); + + while (queue.Count > 0) + { + var treeNode = queue.Dequeue(); + var skipped = false; + + // Each node could represent a file or a folder. + foreach (TreeNode node in treeNode.Nodes) + { + var folder = node.Tag as DPFolder; + var file = node.Tag as DPFile; + if (folder is not null) queue.Enqueue(node); + else + { + if (determinedFiles.Contains(file!)) + nodes.Add(node); + else skipped = true; + } + } + // If we didn't skip anything, then also mark the entire folder. + if (!skipped) nodes.Add(treeNode); + } + + + BeginInvoke(() => + { + treeView1.BeginUpdate(); + foreach (var node in nodes) + { + node.ForeColor = Color.Blue; + } + treeView1.EndUpdate(); + }); + } + + private void colorExtractedToTarget(DPArchive arc) + { + List nodes = new(arc.Contents.Count); + // initialize the queue with root-level nodes first; the root-level nodes + // represent all of the archives processed (which can happen if there are multiple archives in the root) + Queue queue = new Queue(treeView1.Nodes.Cast()); + + while (queue.Count > 0) + { + var treeNode = queue.Dequeue(); + var skipped = false; + + // Each node could represent a file or a folder. + foreach (TreeNode node in treeNode.Nodes) + { + var folder = node.Tag as DPFolder; + var file = node.Tag as DPFile; + if (folder is not null) queue.Enqueue(node); + else + { + if (file!.ExtractedToTarget) nodes.Add(node); + else skipped = true; + } + } + // If we didn't skip anything, then also mark the entire folder. + if (!skipped) nodes.Add(treeNode); + } + + BeginInvoke(() => + { + treeView1.BeginUpdate(); + foreach (var node in nodes) + { + node.ForeColor = Color.Green; + node.NodeFont = new Font(treeView1.Font, FontStyle.Bold); + } + treeView1.EndUpdate(); + }); + } + + private void listProcessedFiles(List reports) + { + if (reports.Count == 0 || reports[0].ExtractedFiles.Count == 0) + { + BeginInvoke(processedTxtBox.Clear); + return; + } + var sb = new StringBuilder(reports.Count * (1 + reports[0].ExtractedFiles.Last().Path.Length)); + foreach (var report in reports) + { + foreach (var file in report.ExtractedFiles) + { + sb.Append(file.AssociatedArchive!.FileName).Append(':').AppendLine(file.Path); + } + } + var str = sb.ToString(); + BeginInvoke(() => processedTxtBox.Text = str); + } + + #region Tasks + private void peekTask() + { + lastRootArchive = setupArchive(); + var settings = new + { + ContentFolders = this.settings.ContentFolders, + ContentRedirectFolders = this.settings.ContentRedirectFolders, + InstallOption = this.settings.InstallOption, + OverwriteFiles = this.settings.OverwriteFiles, + ForceFileToDest = this.settings.ForceFileToDest, + TempPath = this.settings.TempPath, + DestinationPath = this.settings.DestinationPath + }; + Log.Information("Beginning to peek at archive contents."); + Log.Information("Archive File: {lastRootArchive}", lastRootArchive.FileName); + Log.Information("Settings to use: \n{@Settings}", JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true })); + try + { + lastRootArchive.Extractor!.CancellationToken = tokenSource.Token; + lastRootArchive.PeekContents(); + } + catch (Exception ex) + { + Log.Error(ex, "An error occurred while peeking at archive contents"); + } + Log.Information("Finished peeking at archive contents."); + BeginInvoke(treeView1.Nodes.Clear); + buildTree(lastRootArchive); + } + + private void determineDestinationsTask() + { + lastRootArchive ??= setupArchive(); + var settings = new + { + ContentFolders = this.settings.ContentFolders, + ContentRedirectFolders = this.settings.ContentRedirectFolders, + InstallOption = this.settings.InstallOption, + OverwriteFiles = this.settings.OverwriteFiles, + ForceFileToDest = this.settings.ForceFileToDest, + TempPath = this.settings.TempPath, + DestinationPath = this.settings.DestinationPath + }; + Log.Information("Beginning to determine destinations."); + Log.Information("Archive File: {lastRootArchive}", lastRootArchive.FileName); + Log.Information("Settings to use: \n{@Settings}", JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true })); + HashSet determinedFiles = new(); + try + { + if (lastRootArchive.Contents.Count == 0) peekRecursivelyTask(); + determinedFiles = new RecursiveDestinationDeterminer().DetermineDestinations(lastRootArchive, this.settings); + } + catch (Exception ex) + { + Log.Error(ex, "An error occurred while determining destinations"); + } + Log.Information("Finished determining destinations."); + BeginInvoke(treeView1.Nodes.Clear); + buildTree(lastRootArchive); + if (determinedFiles.Count != 0) colorDetermined(determinedFiles); + } + + private void extractTask() + { + lastRootArchive = setupArchive(); + var settings = new + { + ContentFolders = this.settings.ContentFolders, + ContentRedirectFolders = this.settings.ContentRedirectFolders, + InstallOption = this.settings.InstallOption, + OverwriteFiles = this.settings.OverwriteFiles, + ForceFileToDest = this.settings.ForceFileToDest, + TempPath = this.settings.TempPath, + DestinationPath = this.settings.DestinationPath + }; + Log.Information("Beginning to extract archive."); + Log.Information("Archive File: {lastRootArchive}", lastRootArchive.FileName); + Log.Information("Settings to use: \n{@Settings}", JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true })); + try + { + lastRootArchive.Extractor!.CancellationToken = tokenSource.Token; + lastRootArchive.PeekContents(); + var extractSettings = new DPExtractSettings(settings.TempPath, lastRootArchive.Contents.Values); + + // Set the TargetPath for each file. + foreach (var file in lastRootArchive.Contents.Values) + { + file.TargetPath = Path.Combine(settings.DestinationPath, file.Path); + } + + lastRootArchive.ExtractContents(extractSettings); + } + catch (Exception ex) + { + Log.Error(ex, "An error occurred while extracting archive"); + } + Log.Information("Finished extracting archive."); + BeginInvoke(treeView1.Nodes.Clear); + buildTree(lastRootArchive); + colorExtractedToTarget(lastRootArchive); + } + + private void processTask() + { + var autoSave = autoSaveBtn.Checked; + var arcs = new List(); + var records = new List(); + var settings = new + { + ContentFolders = this.settings.ContentFolders, + ContentRedirectFolders = this.settings.ContentRedirectFolders, + InstallOption = this.settings.InstallOption, + OverwriteFiles = this.settings.OverwriteFiles, + ForceFileToDest = this.settings.ForceFileToDest, + TempPath = this.settings.TempPath, + DestinationPath = this.settings.DestinationPath + }; + Log.Information("Beginning to process archive."); + Log.Information("Archive File: {lastRootArchive}", archiveTxtBox.Text); + Log.Information("Settings to use: \n{@Settings}", JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true })); + var processor = currentProcessor = new DPProcessor(); + try + { + processor.ArchiveEnter += (_, a) => arcs.Add(a.Archive); + processor.ArchiveExit += (_, a) => + { + if (a.Processed) records.Add(a.Report!); + }; + processor.ProcessArchive(archiveTxtBox.Text, this.settings); + } + catch (Exception ex) + { + Log.Error(ex, "An error occurred while extracting archive"); + } + Log.Information("Finished processing archive."); + BeginInvoke(treeView1.Nodes.Clear); + currentProcessor = null; + if (arcs.Count == 0) return; + lastRootArchive = arcs[0]; + lastSession = new(processor, lastRootArchive, this.settings, records); + if (autoSave) saveProcess(); + buildTree(arcs[0]); + colorExtractedToTarget(arcs[0]); + listProcessedFiles(records); + } + + private void peekRecursivelyTask() + { + lastRootArchive = setupArchive(); + var settings = new + { + ContentFolders = this.settings.ContentFolders, + ContentRedirectFolders = this.settings.ContentRedirectFolders, + InstallOption = this.settings.InstallOption, + OverwriteFiles = this.settings.OverwriteFiles, + ForceFileToDest = this.settings.ForceFileToDest, + TempPath = this.settings.TempPath, + DestinationPath = this.settings.DestinationPath + }; + Log.Information("Beginning to peek recursively."); + Log.Information("Archive File: {lastRootArchive}", lastRootArchive.FileName); + Log.Information("Settings to use: \n{@Settings}", JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true })); + var queue = new Queue(); + var arcTrees = new List(); + queue.Enqueue(lastRootArchive); + try + { + var token = tokenSource.Token; + while (queue.Count != 0) + { + if (token.IsCancellationRequested) break; + var arc = queue.Dequeue(); + arc.Extractor!.CancellationToken = tokenSource.Token; + arc.PeekContents(settings.TempPath); + arcTrees.Add(arc); + foreach (var nestedArc in arc.Subarchives) + { + queue.Enqueue(nestedArc); + } + } + } + catch (Exception ex) + { + Log.Error(ex, "An error occurred while peeking at archive contents"); + } + Log.Information("Finished recursive peek."); + BeginInvoke(treeView1.Nodes.Clear); + buildTree(lastRootArchive); + } + + #endregion + + private void MainForm_DragEnter(object sender, DragEventArgs e) + { + if (e.Data is not null && e.Data.GetDataPresent(DataFormats.FileDrop)) + e.Effect = DragDropEffects.Copy; + } + + private void MainForm_DragDrop(object sender, DragEventArgs e) + { + if (e.Data is null || !e.Data.GetDataPresent(DataFormats.FileDrop)) return; + string[] files = (string[])e.Data.GetData(DataFormats.FileDrop); + if (files.Length != 1) return; + archiveTxtBox.Text = files[0]; + } + + private void cancelBtn_Click(object sender, EventArgs e) + { + currentProcessor?.CancelProcessing(); + lastSession?.Processor.CancelProcessing(); + tokenSource.Cancel(); + tokenSource = new(); + } + } +} diff --git a/src/DAZ_Installer.TestingSuiteWindows/MainForm.resx b/src/DAZ_Installer.TestingSuiteWindows/MainForm.resx new file mode 100644 index 0000000..4adca14 --- /dev/null +++ b/src/DAZ_Installer.TestingSuiteWindows/MainForm.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + + 183, 17 + + + 497, 17 + + + 385, 17 + + \ No newline at end of file diff --git a/src/DAZ_Installer.TestingSuiteWindows/ProcessSettingsDialogue.Designer.cs b/src/DAZ_Installer.TestingSuiteWindows/ProcessSettingsDialogue.Designer.cs new file mode 100644 index 0000000..4415ab5 --- /dev/null +++ b/src/DAZ_Installer.TestingSuiteWindows/ProcessSettingsDialogue.Designer.cs @@ -0,0 +1,305 @@ +namespace DAZ_Installer.TestingSuiteWindows +{ + partial class ProcessSettingsDialogue + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + label1 = new Label(); + destTxtBox = new TextBox(); + tmpTxtBox = new TextBox(); + label2 = new Label(); + label3 = new Label(); + installOptionComboBox = new ComboBox(); + overwriteChkBox = new CheckBox(); + label4 = new Label(); + cfListBox = new ListBox(); + contextMenuStrip1 = new ContextMenuStrip(components); + removeToolStripMenuItem = new ToolStripMenuItem(); + removeAllToolStripMenuItem = new ToolStripMenuItem(); + copyToolStripMenuItem = new ToolStripMenuItem(); + cfTxtBox = new TextBox(); + addCFBtn = new Button(); + label5 = new Label(); + cfaListBox = new ListBox(); + cfATxtBox = new TextBox(); + label6 = new Label(); + cfaComboBox = new ComboBox(); + addCFABtn = new Button(); + contextMenuStrip1.SuspendLayout(); + SuspendLayout(); + // + // label1 + // + label1.AutoSize = true; + label1.Location = new Point(12, 9); + label1.Name = "label1"; + label1.Size = new Size(94, 15); + label1.TabIndex = 0; + label1.Text = "Destination Path"; + // + // destTxtBox + // + destTxtBox.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + destTxtBox.Location = new Point(112, 6); + destTxtBox.Name = "destTxtBox"; + destTxtBox.ReadOnly = true; + destTxtBox.Size = new Size(376, 23); + destTxtBox.TabIndex = 1; + // + // tmpTxtBox + // + tmpTxtBox.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + tmpTxtBox.Location = new Point(112, 40); + tmpTxtBox.Name = "tmpTxtBox"; + tmpTxtBox.ReadOnly = true; + tmpTxtBox.Size = new Size(376, 23); + tmpTxtBox.TabIndex = 3; + // + // label2 + // + label2.AutoSize = true; + label2.Location = new Point(43, 43); + label2.Name = "label2"; + label2.Size = new Size(63, 15); + label2.TabIndex = 2; + label2.Text = "Temp Path"; + // + // label3 + // + label3.AutoSize = true; + label3.Location = new Point(28, 79); + label3.Name = "label3"; + label3.Size = new Size(78, 15); + label3.TabIndex = 4; + label3.Text = "Install Option"; + // + // installOptionComboBox + // + installOptionComboBox.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + installOptionComboBox.DropDownStyle = ComboBoxStyle.DropDownList; + installOptionComboBox.FormattingEnabled = true; + installOptionComboBox.Items.AddRange(new object[] { "Manifest Only", "Automatic", "File Sense Only" }); + installOptionComboBox.Location = new Point(112, 76); + installOptionComboBox.Name = "installOptionComboBox"; + installOptionComboBox.Size = new Size(376, 23); + installOptionComboBox.TabIndex = 5; + installOptionComboBox.SelectedIndexChanged += installOptionComboBox_SelectedIndexChanged; + // + // overwriteChkBox + // + overwriteChkBox.Anchor = AnchorStyles.Bottom | AnchorStyles.Left; + overwriteChkBox.AutoSize = true; + overwriteChkBox.Location = new Point(12, 471); + overwriteChkBox.Name = "overwriteChkBox"; + overwriteChkBox.Size = new Size(202, 19); + overwriteChkBox.TabIndex = 6; + overwriteChkBox.Text = "Overwrite Files (Destination Only)"; + overwriteChkBox.UseVisualStyleBackColor = true; + overwriteChkBox.CheckedChanged += overwriteChkBox_CheckedChanged; + // + // label4 + // + label4.AutoSize = true; + label4.Location = new Point(15, 112); + label4.Name = "label4"; + label4.Size = new Size(91, 15); + label4.TabIndex = 7; + label4.Text = "Content Folders"; + // + // cfListBox + // + cfListBox.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + cfListBox.ContextMenuStrip = contextMenuStrip1; + cfListBox.FormattingEnabled = true; + cfListBox.ItemHeight = 15; + cfListBox.Location = new Point(112, 112); + cfListBox.Name = "cfListBox"; + cfListBox.Size = new Size(376, 139); + cfListBox.TabIndex = 8; + // + // contextMenuStrip1 + // + contextMenuStrip1.Items.AddRange(new ToolStripItem[] { removeToolStripMenuItem, removeAllToolStripMenuItem, copyToolStripMenuItem }); + contextMenuStrip1.Name = "contextMenuStrip1"; + contextMenuStrip1.Size = new Size(135, 70); + // + // removeToolStripMenuItem + // + removeToolStripMenuItem.Name = "removeToolStripMenuItem"; + removeToolStripMenuItem.Size = new Size(134, 22); + removeToolStripMenuItem.Text = "Remove"; + removeToolStripMenuItem.Click += removeToolStripMenuItem_Click; + // + // removeAllToolStripMenuItem + // + removeAllToolStripMenuItem.Name = "removeAllToolStripMenuItem"; + removeAllToolStripMenuItem.Size = new Size(134, 22); + removeAllToolStripMenuItem.Text = "Remove All"; + removeAllToolStripMenuItem.Click += removeAllToolStripMenuItem_Click; + // + // copyToolStripMenuItem + // + copyToolStripMenuItem.Name = "copyToolStripMenuItem"; + copyToolStripMenuItem.Size = new Size(134, 22); + copyToolStripMenuItem.Text = "Copy"; + copyToolStripMenuItem.Click += copyToolStripMenuItem_Click; + // + // cfTxtBox + // + cfTxtBox.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + cfTxtBox.Location = new Point(112, 257); + cfTxtBox.Name = "cfTxtBox"; + cfTxtBox.Size = new Size(319, 23); + cfTxtBox.TabIndex = 9; + // + // addCFBtn + // + addCFBtn.Anchor = AnchorStyles.Top | AnchorStyles.Right; + addCFBtn.Location = new Point(437, 257); + addCFBtn.Name = "addCFBtn"; + addCFBtn.Size = new Size(51, 23); + addCFBtn.TabIndex = 10; + addCFBtn.Text = "Add"; + addCFBtn.UseVisualStyleBackColor = true; + addCFBtn.Click += addCFBtn_Click; + // + // label5 + // + label5.Location = new Point(15, 284); + label5.Name = "label5"; + label5.Size = new Size(91, 34); + label5.TabIndex = 11; + label5.Text = "Content Folder Aliases"; + // + // cfaListBox + // + cfaListBox.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + cfaListBox.ContextMenuStrip = contextMenuStrip1; + cfaListBox.FormattingEnabled = true; + cfaListBox.ItemHeight = 15; + cfaListBox.Location = new Point(112, 286); + cfaListBox.Name = "cfaListBox"; + cfaListBox.Size = new Size(376, 139); + cfaListBox.TabIndex = 12; + // + // cfATxtBox + // + cfATxtBox.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + cfATxtBox.Location = new Point(112, 431); + cfATxtBox.Name = "cfATxtBox"; + cfATxtBox.Size = new Size(150, 23); + cfATxtBox.TabIndex = 13; + // + // label6 + // + label6.Anchor = AnchorStyles.Top | AnchorStyles.Right; + label6.AutoSize = true; + label6.Location = new Point(268, 434); + label6.Name = "label6"; + label6.Size = new Size(18, 15); + label6.TabIndex = 14; + label6.Text = "to"; + // + // cfaComboBox + // + cfaComboBox.Anchor = AnchorStyles.Top | AnchorStyles.Right; + cfaComboBox.DropDownStyle = ComboBoxStyle.DropDownList; + cfaComboBox.FormattingEnabled = true; + cfaComboBox.Location = new Point(292, 431); + cfaComboBox.Name = "cfaComboBox"; + cfaComboBox.Size = new Size(139, 23); + cfaComboBox.TabIndex = 15; + // + // addCFABtn + // + addCFABtn.Anchor = AnchorStyles.Top | AnchorStyles.Right; + addCFABtn.Location = new Point(437, 430); + addCFABtn.Name = "addCFABtn"; + addCFABtn.Size = new Size(51, 23); + addCFABtn.TabIndex = 16; + addCFABtn.Text = "Add"; + addCFABtn.UseVisualStyleBackColor = true; + addCFABtn.Click += addCFABtn_Click; + // + // ProcessSettingsDialogue + // + AutoScaleDimensions = new SizeF(96F, 96F); + AutoScaleMode = AutoScaleMode.Dpi; + ClientSize = new Size(500, 497); + Controls.Add(addCFABtn); + Controls.Add(cfaComboBox); + Controls.Add(label6); + Controls.Add(cfATxtBox); + Controls.Add(cfaListBox); + Controls.Add(label5); + Controls.Add(addCFBtn); + Controls.Add(cfTxtBox); + Controls.Add(cfListBox); + Controls.Add(label4); + Controls.Add(overwriteChkBox); + Controls.Add(installOptionComboBox); + Controls.Add(label3); + Controls.Add(tmpTxtBox); + Controls.Add(label2); + Controls.Add(destTxtBox); + Controls.Add(label1); + MaximizeBox = false; + MinimumSize = new Size(516, 536); + Name = "ProcessSettingsDialogue"; + Text = "Process Settings Dialogue"; + Load += ProcessSettingsDialogue_Load; + contextMenuStrip1.ResumeLayout(false); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private Label label1; + private TextBox destTxtBox; + private TextBox tmpTxtBox; + private Label label2; + private Label label3; + private ComboBox installOptionComboBox; + private CheckBox overwriteChkBox; + private Label label4; + private ListBox cfListBox; + private TextBox cfTxtBox; + private Button addCFBtn; + private Label label5; + private ListBox cfaListBox; + private TextBox cfATxtBox; + private Label label6; + private ComboBox cfaComboBox; + private Button addCFABtn; + private ContextMenuStrip contextMenuStrip1; + private ToolStripMenuItem removeToolStripMenuItem; + private ToolStripMenuItem removeAllToolStripMenuItem; + private ToolStripMenuItem copyToolStripMenuItem; + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.TestingSuiteWindows/ProcessSettingsDialogue.cs b/src/DAZ_Installer.TestingSuiteWindows/ProcessSettingsDialogue.cs new file mode 100644 index 0000000..036a0e0 --- /dev/null +++ b/src/DAZ_Installer.TestingSuiteWindows/ProcessSettingsDialogue.cs @@ -0,0 +1,173 @@ +using DAZ_Installer.Core; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace DAZ_Installer.TestingSuiteWindows +{ + public partial class ProcessSettingsDialogue : Form + { + public DPProcessSettings Settings; + public static HashSet InvalidChars = new(Path.GetInvalidFileNameChars()); + HashSet keys = null!; + public ProcessSettingsDialogue() => InitializeComponent(); + + public ProcessSettingsDialogue(DPProcessSettings settings) : this() => Settings = settings; + + private void ProcessSettingsDialogue_Load(object sender, EventArgs e) + { + keys = new(Settings.ContentRedirectFolders!.Keys, StringComparer.OrdinalIgnoreCase); + SuspendLayout(); + destTxtBox.Text = Settings.DestinationPath; + tmpTxtBox.Text = Settings.TempPath; + cfListBox.Items.AddRange(Settings.ContentFolders!.ToArray()); + cfaListBox.Items.AddRange(Settings.ContentRedirectFolders.Select(x => $"{x.Key} --> {x.Value}").ToArray()); + cfaComboBox.Items.AddRange(Settings.ContentFolders.ToArray()); + switch (Settings.InstallOption) + { + case InstallOptions.ManifestOnly: + installOptionComboBox.SelectedIndex = 0; + break; + case InstallOptions.ManifestAndAuto: + installOptionComboBox.SelectedIndex = 1; + break; + case InstallOptions.Automatic: + installOptionComboBox.SelectedIndex = 2; + break; + } + overwriteChkBox.Checked = Settings.OverwriteFiles; + ResumeLayout(); + } + + private void installOptionComboBox_SelectedIndexChanged(object sender, EventArgs e) + { + Settings.InstallOption = installOptionComboBox.SelectedIndex switch + { + 0 => InstallOptions.ManifestOnly, + 1 => InstallOptions.ManifestAndAuto, + 2 => InstallOptions.Automatic, + _ => throw new NotImplementedException() + }; + } + + private void addCFBtn_Click(object sender, EventArgs e) + { + SuspendLayout(); + var txt = cfTxtBox.Text.Trim(); + if (txt.Length == 0) return; + if (Settings.ContentFolders!.Contains(txt)) + { + MessageBox.Show("Cannot add duplicate content folders; a content folder with that name already exists.", + "Name already exists", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + for (var i = 0; i < txt.Length; i++) + { + if (InvalidChars.Contains(txt[i])) + { + MessageBox.Show("Cannot add content folder due to forbidden characters in name.", + "Forbidden characters not allowed by OS", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + } + cfListBox.Items.Add(cfTxtBox.Text); + cfaComboBox.Items.Add(cfTxtBox.Text); + Settings.ContentFolders.Add(cfTxtBox.Text); + cfTxtBox.Text = string.Empty; + ResumeLayout(); + } + + private void addCFABtn_Click(object sender, EventArgs e) + { + SuspendLayout(); + var aliasTxt = cfATxtBox.Text.Trim(); + var comboTxt = cfaComboBox.SelectedItem.ToString()!.Trim() ?? string.Empty; + if (cfaComboBox.SelectedIndex == -1 || aliasTxt.Length == 0 || comboTxt.Length == 0) return; + + if (keys.Contains(aliasTxt)) + { + MessageBox.Show($"Alias {aliasTxt} already exists; cannot use duplicate aliases.", + "Alias already exists", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + Settings.ContentRedirectFolders![aliasTxt] = comboTxt; + keys.Add(aliasTxt); + cfaListBox.BeginUpdate(); + cfaListBox.Items.Add($"{aliasTxt} --> {comboTxt}"); + cfaListBox.EndUpdate(); + ResumeLayout(); + } + + private void overwriteChkBox_CheckedChanged(object sender, EventArgs e) + { + Settings.OverwriteFiles = overwriteChkBox.Checked; + } + + private void removeToolStripMenuItem_Click(object sender, EventArgs e) + { + ListBox box = (ListBox)((sender as ToolStripMenuItem).Owner as ContextMenuStrip).SourceControl; + var toDeleteItems = new string[box.SelectedItems.Count]; + box.SelectedItems.CopyTo(toDeleteItems, 0); + box.BeginUpdate(); + foreach (var item in toDeleteItems) + { + if (box.SelectedItems[0].ToString().Contains(" --> ")) + { + var tokens = item.Split(" --> "); + var key = tokens[0]; + keys.TryGetValue(key, out var realKey); + Settings.ContentRedirectFolders!.Remove(realKey); + keys.Remove(realKey); + box.Items.Remove(item); + } + else + { + Settings.ContentFolders!.Remove(item); + box.Items.Remove(item); + cfaComboBox.Items.Remove(item); + } + } + box.EndUpdate(); + } + + private void removeAllToolStripMenuItem_Click(object sender, EventArgs e) + { + ListBox box = (ListBox)((sender as ToolStripMenuItem).Owner as ContextMenuStrip).SourceControl; + box.BeginUpdate(); + if (box.SelectedItems[0].ToString().Contains(" --> ")) + { + Settings.ContentRedirectFolders!.Clear(); + keys.Clear(); + } + else + { + Settings.ContentFolders!.Clear(); + cfaComboBox.Items.Clear(); + } + box.Items.Clear(); + box.EndUpdate(); + } + + private void copyToolStripMenuItem_Click(object sender, EventArgs e) + { + ListBox box = (ListBox)((sender as ToolStripMenuItem).Owner as ContextMenuStrip).SourceControl; + if (box.SelectedItems.Count == 0) return; + if (box.SelectedItems.Count == 1) Clipboard.SetText((string) box.SelectedItems[0]); + else + { + var sb = new StringBuilder(); + foreach (string item in box.SelectedItems) + { + sb.AppendLine(item); + } + Clipboard.SetText(sb.ToString()); + } + } + } +} diff --git a/src/DAZ_Installer.TestingSuiteWindows/ProcessSettingsDialogue.resx b/src/DAZ_Installer.TestingSuiteWindows/ProcessSettingsDialogue.resx new file mode 100644 index 0000000..8b39395 --- /dev/null +++ b/src/DAZ_Installer.TestingSuiteWindows/ProcessSettingsDialogue.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file diff --git a/src/DAZ_Installer.TestingSuiteWindows/Program.cs b/src/DAZ_Installer.TestingSuiteWindows/Program.cs new file mode 100644 index 0000000..280e8b6 --- /dev/null +++ b/src/DAZ_Installer.TestingSuiteWindows/Program.cs @@ -0,0 +1,34 @@ +using Serilog; +using DAZ_Installer.Core; + +namespace DAZ_Installer.TestingSuiteWindows +{ + internal static class Program + { + internal static readonly string TempPath = Path.Combine(Path.GetTempPath(), "DAZ_Installer_TestingSuiteWindows"); + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .MinimumLevel.Debug() +#if DEBUG + .WriteTo.Debug(SerilogLoggerConstants.Template) +#endif + .WriteTo.Sink(new RichTextBoxSink()) + .WriteTo.Async(a => a.File(SerilogLoggerConstants.Template, "log.txt", + fileSizeLimitBytes: 20 * 1024 * 1024, // 20 MB + rollOnFileSizeLimit: true, + retainedFileCountLimit: 5, + retainedFileTimeLimit: TimeSpan.FromDays(5))) + .CreateLogger(); + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + Application.Run(new MainForm()); + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.TestingSuiteWindows/RecursiveDestinationDeterminer.cs b/src/DAZ_Installer.TestingSuiteWindows/RecursiveDestinationDeterminer.cs new file mode 100644 index 0000000..f9b0390 --- /dev/null +++ b/src/DAZ_Installer.TestingSuiteWindows/RecursiveDestinationDeterminer.cs @@ -0,0 +1,27 @@ +using DAZ_Installer.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DAZ_Installer.TestingSuiteWindows +{ + /// + /// A destination determiner that uses to determine the destination of each file in the archive. + /// This is a recursive destination determiner, meaning that it will also determine the destinations of subarchives/archives within the given archive. + /// + internal class RecursiveDestinationDeterminer : AbstractDestinationDeterminer + { + /// + public override HashSet DetermineDestinations(DPArchive arc, DPProcessSettings settings) + { + var hash = new DPDestinationDeterminerEx().DetermineDestinations(arc, settings); + foreach (var subarc in arc.Subarchives.Where(x => x.Extracted)) + { + hash.UnionWith(new DPDestinationDeterminerEx().DetermineDestinations(subarc, settings)); + } + return hash; + } + } +} diff --git a/src/DAZ_Installer.TestingSuiteWindows/ResultCompiler.cs b/src/DAZ_Installer.TestingSuiteWindows/ResultCompiler.cs new file mode 100644 index 0000000..1a69ecc --- /dev/null +++ b/src/DAZ_Installer.TestingSuiteWindows/ResultCompiler.cs @@ -0,0 +1,68 @@ +using System; +using System.Text.Json; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DAZ_Installer.IO; +using DAZ_Installer.Core.Extraction; +using DAZ_Installer.Core; + +namespace DAZ_Installer.TestingSuiteWindows +{ + internal static class ResultCompiler + { + public static string CompileResults(IEnumerable extractionReports, DPProcessSettings settings, DPArchive arc) + { + var l = new List(); + foreach (var report in extractionReports) + { + l.Add(new DPArchiveMap + { + ArchiveName = report.Settings.Archive.FileName, + Mappings = report.ExtractedFiles.ToDictionary(x => x.Path, x => x.RelativePathToContentFolder) + }); + } + + return new DPProcessorTestManifest(settings, arc.FileName, l).ToJson(); + } + + public static string CompileResults(TreeNodeCollection rootNodes, DPProcessSettings settings, DPArchive arc) + { + var l = new List(); + foreach (TreeNode rootNode in rootNodes) + { + l.Add(new DPArchiveMap + { + ArchiveName = rootNode.Text, + Mappings = makeMappings(rootNode) + }); + } + + return new DPProcessorTestManifest(settings, arc.FileName, l).ToJson(); + } + + private static Dictionary makeMappings(TreeNode node) + { + if (node.Nodes.Count == 0) return new Dictionary(); + var d = new Dictionary(((DPAbstractNode) node.Nodes[0].Tag).AssociatedArchive!.Contents.Count); + + // Create an iterative approach to this. + var stack = new Stack(); + stack.Push(node); + + while (stack.Count > 0) + { + var currentNode = stack.Pop(); + if (currentNode.ForeColor != Color.Green || currentNode.Tag is not DPFile file) continue; + d[file.Path] = file.RelativePathToContentFolder; + foreach (TreeNode childNode in currentNode.Nodes) + { + stack.Push(childNode); + } + } + + return d; + } + } +} diff --git a/src/DAZ_Installer.TestingSuiteWindows/RichTextBoxSink.cs b/src/DAZ_Installer.TestingSuiteWindows/RichTextBoxSink.cs new file mode 100644 index 0000000..0f3f47f --- /dev/null +++ b/src/DAZ_Installer.TestingSuiteWindows/RichTextBoxSink.cs @@ -0,0 +1,29 @@ +using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DAZ_Installer.TestingSuiteWindows +{ + internal class RichTextBoxSink : ILogEventSink + { + private ITextFormatter formatter = SerilogLoggerConstants.Template; + private RichTextBox? _textBox; + + public RichTextBoxSink() { } + + public void Emit(LogEvent logEvent) + { + _textBox ??= MainForm.Instance?.logOutputTxtBox ?? null; + if (_textBox is null || !_textBox.IsHandleCreated) return; + + using StringWriter stringWriter = new StringWriter(); + formatter.Format(logEvent, stringWriter); + _textBox.BeginInvoke(() => _textBox.AppendText(stringWriter.ToString())); + } + } +} diff --git a/src/DAZ_Installer.UI/DAZ_Installer.UI.csproj b/src/DAZ_Installer.UI/DAZ_Installer.UI.csproj new file mode 100644 index 0000000..7366d1e --- /dev/null +++ b/src/DAZ_Installer.UI/DAZ_Installer.UI.csproj @@ -0,0 +1,37 @@ + + + + net6.0-windows + enable + true + enable + + + + + + + + + + + + + + + + + UserControl + + + UserControl + + + UserControl + + + UserControl + + + + diff --git a/src/Custom Controls/LibraryItem.Designer.cs b/src/DAZ_Installer.UI/LibraryItem.Designer.cs similarity index 100% rename from src/Custom Controls/LibraryItem.Designer.cs rename to src/DAZ_Installer.UI/LibraryItem.Designer.cs diff --git a/src/Custom Controls/LibraryItem.cs b/src/DAZ_Installer.UI/LibraryItem.cs similarity index 76% rename from src/Custom Controls/LibraryItem.cs rename to src/DAZ_Installer.UI/LibraryItem.cs index 5ff36cb..fa7bdf9 100644 --- a/src/Custom Controls/LibraryItem.cs +++ b/src/DAZ_Installer.UI/LibraryItem.cs @@ -1,13 +1,8 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE -using System; -using System.IO; -using System.Collections.Generic; +using DAZ_Installer.Database; using System.ComponentModel; -using System.Windows.Forms; -using System.Drawing; -using DAZ_Installer.DP; namespace DAZ_Installer { @@ -41,9 +36,18 @@ public string[] Tags set => UpdateTags(value); } - internal DPProductRecord ProductRecord { get; set; } + [Description("Determines the maximum number of tags to display."), Category("Data"), Browsable(true)] + [DefaultValue(4)] + public uint MaxTagCount { get; set; } = 4; + public DPDatabase? Database { get; set; } - private readonly List
public partial class LibraryPanel : UserControl { - public LibraryPanel() - { - InitializeComponent(); - } + public LibraryPanel() => InitializeComponent(); [Browsable(true), EditorBrowsable(EditorBrowsableState.Always), Description("Holds the current library items."), Category("Items")] - internal List LibraryItems { get; } = new List(25); + public List LibraryItems { get; } = new List(25); - internal List SearchItems { get; set; } = new List(25); - internal uint CurrentPage - { - get => pageButtonControl1.CurrentPage; - set - { - pageButtonControl1.CurrentPage = value; - } + public List SearchItems { get; set; } = new List(25); + public uint CurrentPage + { + get => pageButtonControl1.CurrentPage; + set => pageButtonControl1.CurrentPage = value; } - internal uint PageCount + public uint PageCount { get => pageButtonControl1.PageCount; set => pageButtonControl1.PageCount = value; } - internal bool EditMode + public bool EditMode { set { @@ -48,7 +38,8 @@ internal bool EditMode if (value == true) { mainContentPanel.SuspendLayout(); - } else + } + else { mainContentPanel.ResumeLayout(true); } @@ -58,34 +49,35 @@ internal bool EditMode get => editMode; } private bool editMode; - internal volatile bool SearchMode = false; + public volatile bool SearchMode = false; - internal void UpdateMainContent() + public void UpdateMainContent() { EditMode = true; if (!SearchMode) { - DPCommon.WriteToLog("Update main content."); - DPCommon.WriteToLog($"Library items: {LibraryItems.Count}"); - + //// DPCommon.WriteToLog("Update main content."); + //// DPCommon.WriteToLog($"Library items: {LibraryItems.Count}"); + mainContentPanel.Controls.Clear(); mainContentPanel.Controls.AddRange(LibraryItems.ToArray()); - foreach (var item in LibraryItems) + foreach (LibraryItem item in LibraryItems) { if (item != null) item.Dock = DockStyle.Top; } - - } else + + } + else { - DPCommon.WriteToLog("Update search content."); - DPCommon.WriteToLog($"Search items: {SearchItems.Count}"); + //// DPCommon.WriteToLog("Update search content."); + //// DPCommon.WriteToLog($"Search items: {SearchItems.Count}"); mainContentPanel.Controls.Clear(); mainContentPanel.Controls.AddRange(SearchItems.ToArray()); - foreach (var item in SearchItems) + foreach (LibraryItem item in SearchItems) { if (item != null) item.Dock = DockStyle.Top; @@ -94,24 +86,15 @@ internal void UpdateMainContent() EditMode = false; } - internal void NudgeCurrentPage(uint page) - { - pageButtonControl1.SilentUpdateCurrentPage(page); - } + public void NudgeCurrentPage(uint page) => pageButtonControl1.SilentUpdateCurrentPage(page); - internal void NudgePageCount(uint count) => pageButtonControl1.SilentUpdatePageCount(count); + public void NudgePageCount(uint count) => pageButtonControl1.SilentUpdatePageCount(count); - private void pageButtonControl1_SizeChanged(object sender, EventArgs e) - { + private void pageButtonControl1_SizeChanged(object sender, EventArgs e) => // We need to manually center it in the containing panel. - pageButtonControl1.Left = (buttonsContainer.ClientSize.Width - pageButtonControl1.Width) / 2; - //pageButtonControl1.Top = (buttonsContainer.ClientSize.Height - pageButtonControl1.Height) / 2; - } + pageButtonControl1.Left = (buttonsContainer.ClientSize.Width - pageButtonControl1.Width) / 2;//pageButtonControl1.Top = (buttonsContainer.ClientSize.Height - pageButtonControl1.Height) / 2; - internal void AddPageChangeListener(PageButtonControl.PageChangeHandler pageChangedFunc) - { - pageButtonControl1.PageChange += pageChangedFunc; - } + public void AddPageChangeListener(PageButtonControl.PageChangeHandler pageChangedFunc) => pageButtonControl1.PageChange += pageChangedFunc; private void createNewRecordToolStripMenuItem_Click(object sender, EventArgs e) { @@ -119,13 +102,10 @@ private void createNewRecordToolStripMenuItem_Click(object sender, EventArgs e) } // Form.OnResizeEnd may be better performance wise. - private void buttonsContainer_SizeChanged(object sender, EventArgs e) - { + private void buttonsContainer_SizeChanged(object sender, EventArgs e) => // We need to manually center it in the containing panel. // TODO: Hide - pageButtonControl1.Left = (buttonsContainer.ClientSize.Width - pageButtonControl1.Width) / 2; - //pageButtonControl1.Top = (buttonsContainer.ClientSize.Height - pageButtonControl1.Height) / 2; - } + pageButtonControl1.Left = (buttonsContainer.ClientSize.Width - pageButtonControl1.Width) / 2;//pageButtonControl1.Top = (buttonsContainer.ClientSize.Height - pageButtonControl1.Height) / 2; private void toolStripStatusLabel1_Click(object sender, EventArgs e) { diff --git a/src/Custom Controls/LibraryPanel.resx b/src/DAZ_Installer.UI/LibraryPanel.resx similarity index 100% rename from src/Custom Controls/LibraryPanel.resx rename to src/DAZ_Installer.UI/LibraryPanel.resx diff --git a/src/Custom Controls/PageButtonControl.Designer.cs b/src/DAZ_Installer.UI/PageButtonControl.Designer.cs similarity index 100% rename from src/Custom Controls/PageButtonControl.Designer.cs rename to src/DAZ_Installer.UI/PageButtonControl.Designer.cs diff --git a/src/Custom Controls/PageButtonControl.cs b/src/DAZ_Installer.UI/PageButtonControl.cs similarity index 90% rename from src/Custom Controls/PageButtonControl.cs rename to src/DAZ_Installer.UI/PageButtonControl.cs index 49466a9..d47dfe5 100644 --- a/src/Custom Controls/PageButtonControl.cs +++ b/src/DAZ_Installer.UI/PageButtonControl.cs @@ -1,13 +1,9 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; using System.ComponentModel; -using System.Linq; -using System.Windows.Forms; -using static DAZ_Installer.DP.DPCommon; +using System.ComponentModel.DataAnnotations; +//using static DAZ_Installer namespace DAZ_Installer { @@ -15,10 +11,10 @@ namespace DAZ_Installer public partial class PageButtonControl : UserControl { // - internal delegate void PageChangeHandler(uint page); + public delegate void PageChangeHandler(uint page); // // - internal event PageChangeHandler PageChange; + public event PageChangeHandler PageChange; // [Range(1, uint.MaxValue)] [Browsable(true), Description("Gets the current page and setting it calls UpdateControl."), Category("Data"), EditorBrowsable(EditorBrowsableState.Always), DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] @@ -56,15 +52,12 @@ public uint PageCount protected uint pageCount = 1; [ThreadStatic] protected bool invoked = false; - public PageButtonControl() - { - InitializeComponent(); - } + public PageButtonControl() => InitializeComponent(); private bool IsValidPageNumber(uint page) => page <= pageCount && page > 0; private void SwitchPage(object _, EventArgs __) { - if (uint.TryParse(gotoTxtBox.Text, out uint page)) + if (uint.TryParse(gotoTxtBox.Text, out var page)) CurrentPage = page; } @@ -74,7 +67,8 @@ private void SwitchPage(object _, EventArgs __) private void SwitchToLast(object _, EventArgs __) => CurrentPage = pageCount; public void UpdateControl() { - bool isOnMainThread = IsOnMainThread; + var isOnMainThread = false; + //bool isOnMainThread = IsOnMainThread; if ((!isOnMainThread || (IsHandleCreated && InvokeRequired)) && !invoked) { if (!IsHandleCreated) return; // Stop failing to create component on designer. diff --git a/src/Custom Controls/PageButtonControl.resx b/src/DAZ_Installer.UI/PageButtonControl.resx similarity index 100% rename from src/Custom Controls/PageButtonControl.resx rename to src/DAZ_Installer.UI/PageButtonControl.resx diff --git a/src/DAZ_Installer.UI/ProgressCombo.Designer.cs b/src/DAZ_Installer.UI/ProgressCombo.Designer.cs new file mode 100644 index 0000000..2cb46fd --- /dev/null +++ b/src/DAZ_Installer.UI/ProgressCombo.Designer.cs @@ -0,0 +1,102 @@ +namespace DAZ_Installer.UI +{ + partial class ProgressCombo + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + progressBarLbl = new Label(); + progressBar = new ProgressBar(); + cancelBtn = new Button(); + mainProcLbl = new Label(); + SuspendLayout(); + // + // progressBarLbl + // + progressBarLbl.AutoEllipsis = true; + progressBarLbl.Font = new Font("Segoe UI", 10.2F, FontStyle.Regular, GraphicsUnit.Point); + progressBarLbl.Location = new Point(0, 46); + progressBarLbl.Name = "progressBarLbl"; + progressBarLbl.Size = new Size(667, 31); + progressBarLbl.TabIndex = 0; + progressBarLbl.Text = "Processing..."; + progressBarLbl.TextAlign = ContentAlignment.BottomLeft; + // + // progressBar + // + progressBar.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + progressBar.Location = new Point(0, 80); + progressBar.MarqueeAnimationSpeed = 20; + progressBar.Name = "progressBar"; + progressBar.Size = new Size(667, 293); + progressBar.TabIndex = 1; + progressBar.Value = 50; + // + // cancelBtn + // + cancelBtn.Anchor = AnchorStyles.Top | AnchorStyles.Right; + cancelBtn.Font = new Font("Segoe UI", 13.8F, FontStyle.Regular, GraphicsUnit.Point); + cancelBtn.Location = new Point(612, 3); + cancelBtn.Name = "cancelBtn"; + cancelBtn.Size = new Size(52, 46); + cancelBtn.TabIndex = 2; + cancelBtn.Text = "X"; + cancelBtn.UseVisualStyleBackColor = true; + cancelBtn.Click += cancelBtn_Click; + // + // mainProcLbl + // + mainProcLbl.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + mainProcLbl.AutoEllipsis = true; + mainProcLbl.Font = new Font("Segoe UI Variable Display Semil", 17.25F, FontStyle.Regular, GraphicsUnit.Point); + mainProcLbl.Location = new Point(0, 0); + mainProcLbl.Margin = new Padding(4, 0, 4, 0); + mainProcLbl.Name = "mainProcLbl"; + mainProcLbl.Size = new Size(608, 46); + mainProcLbl.TabIndex = 3; + mainProcLbl.Text = "Nothing to extract."; + mainProcLbl.TextAlign = ContentAlignment.MiddleLeft; + // + // ProgressCombo + // + AutoScaleMode = AutoScaleMode.Inherit; + Controls.Add(cancelBtn); + Controls.Add(mainProcLbl); + Controls.Add(progressBar); + Controls.Add(progressBarLbl); + Name = "ProgressCombo"; + Size = new Size(667, 373); + ResumeLayout(false); + } + + #endregion + + private Label progressBarLbl; + private ProgressBar progressBar; + private Button cancelBtn; + internal Label mainProcLbl; + } +} diff --git a/src/DAZ_Installer.UI/ProgressCombo.cs b/src/DAZ_Installer.UI/ProgressCombo.cs new file mode 100644 index 0000000..9bb54cd --- /dev/null +++ b/src/DAZ_Installer.UI/ProgressCombo.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace DAZ_Installer.UI +{ + public partial class ProgressCombo : UserControl + { + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public CancellationTokenSource CancellationTokenSource { get; set; } = new(); + public bool IsMarquee => progressBar.Style == ProgressBarStyle.Marquee; + + public ProgressCombo() + { + InitializeComponent(); + progressBarLbl.Visible = progressBar.Visible = cancelBtn.Visible = false; + } + + public void EndProgress() + { + if (InvokeRequired) + { + BeginInvoke(EndProgress); + return; + } + cancelBtn.Visible = false; + } + + public void StartProgress() + { + if (InvokeRequired) + { + BeginInvoke(StartProgress); + return; + } + progressBarLbl.Visible = progressBar.Visible = cancelBtn.Visible = true; + } + + public void SetProgress(int value) + { + if (InvokeRequired) + { + BeginInvoke(SetProgress, value); + return; + } + progressBar.Value = value; + } + + /// + /// Changes the process bar style to either or ". + /// It automatically checks if Invoke is required. + /// + /// Whether to set the progress bar style to Marqueue or not. + public void ChangeProgressBarStyle(bool marqueue) + { + if (InvokeRequired) + { + BeginInvoke(ChangeProgressBarStyle, marqueue); + return; + } + progressBar.SuspendLayout(); + if (marqueue) + { + progressBar.Value = 10; + progressBar.Style = ProgressBarStyle.Marquee; + } + else + { + progressBar.Value = 50; + progressBar.Style = ProgressBarStyle.Blocks; + } + progressBar.ResumeLayout(); + + } + + /// + /// Sets the text of the progress bar label and the main process label. Automatically checks if Invoke is required. + /// + /// The text to set it to. + public void SetText(string text) + { + if (InvokeRequired) + { + BeginInvoke(SetText, text); + return; + } + SuspendLayout(); + progressBarLbl.Text = mainProcLbl.Text = text; + ResumeLayout(); + } + + private void cancelBtn_Click(object sender, EventArgs e) + { + CancellationTokenSource.Cancel(); + CancellationTokenSource = new(); + } + } +} diff --git a/src/DAZ_Installer.UI/ProgressCombo.resx b/src/DAZ_Installer.UI/ProgressCombo.resx new file mode 100644 index 0000000..af32865 --- /dev/null +++ b/src/DAZ_Installer.UI/ProgressCombo.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/Custom Controls/QueueControl.Designer.cs b/src/DAZ_Installer.UI/QueueControl.Designer.cs similarity index 100% rename from src/Custom Controls/QueueControl.Designer.cs rename to src/DAZ_Installer.UI/QueueControl.Designer.cs diff --git a/src/DAZ_Installer.UI/QueueControl.cs b/src/DAZ_Installer.UI/QueueControl.cs new file mode 100644 index 0000000..3e2a517 --- /dev/null +++ b/src/DAZ_Installer.UI/QueueControl.cs @@ -0,0 +1,10 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +namespace DAZ_Installer.Custom_Controls +{ + public partial class QueueControl : UserControl + { + public QueueControl() => InitializeComponent(); + } +} diff --git a/src/Custom Controls/QueueControl.resx b/src/DAZ_Installer.UI/QueueControl.resx similarity index 100% rename from src/Custom Controls/QueueControl.resx rename to src/DAZ_Installer.UI/QueueControl.resx diff --git a/Assets/ArrowDown.png b/src/DAZ_Installer.Windows/Assets/ArrowDown.png similarity index 100% rename from Assets/ArrowDown.png rename to src/DAZ_Installer.Windows/Assets/ArrowDown.png diff --git a/Assets/ArrowRight.jpg b/src/DAZ_Installer.Windows/Assets/ArrowRight.jpg similarity index 100% rename from Assets/ArrowRight.jpg rename to src/DAZ_Installer.Windows/Assets/ArrowRight.jpg diff --git a/Assets/ArrowRight.png b/src/DAZ_Installer.Windows/Assets/ArrowRight.png similarity index 100% rename from Assets/ArrowRight.png rename to src/DAZ_Installer.Windows/Assets/ArrowRight.png diff --git a/Assets/ArrowUp.png b/src/DAZ_Installer.Windows/Assets/ArrowUp.png similarity index 100% rename from Assets/ArrowUp.png rename to src/DAZ_Installer.Windows/Assets/ArrowUp.png diff --git a/src/DAZ_Installer.Windows/Assets/ArrowUp1.png b/src/DAZ_Installer.Windows/Assets/ArrowUp1.png new file mode 100644 index 0000000000000000000000000000000000000000..6e7dbb41f0e8ed36f813a7417cdbe956a8e35a05 GIT binary patch literal 4365 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 literal 0 HcmV?d00001 diff --git a/Assets/Icon1.ico b/src/DAZ_Installer.Windows/Assets/Icon1.ico similarity index 100% rename from Assets/Icon1.ico rename to src/DAZ_Installer.Windows/Assets/Icon1.ico diff --git a/Assets/Logo2-256x.png b/src/DAZ_Installer.Windows/Assets/Logo2-256x.png similarity index 100% rename from Assets/Logo2-256x.png rename to src/DAZ_Installer.Windows/Assets/Logo2-256x.png diff --git a/Assets/NoImageFound.jpg b/src/DAZ_Installer.Windows/Assets/NoImageFound.jpg similarity index 100% rename from Assets/NoImageFound.jpg rename to src/DAZ_Installer.Windows/Assets/NoImageFound.jpg diff --git a/Assets/RAR-Icon-New-Original-APK.png b/src/DAZ_Installer.Windows/Assets/RAR-Icon-New-Original-APK.png similarity index 100% rename from Assets/RAR-Icon-New-Original-APK.png rename to src/DAZ_Installer.Windows/Assets/RAR-Icon-New-Original-APK.png diff --git a/Assets/WindowsFolderIcon.png b/src/DAZ_Installer.Windows/Assets/WindowsFolderIcon.png similarity index 100% rename from Assets/WindowsFolderIcon.png rename to src/DAZ_Installer.Windows/Assets/WindowsFolderIcon.png diff --git a/Assets/favicon.ico b/src/DAZ_Installer.Windows/Assets/favicon.ico similarity index 100% rename from Assets/favicon.ico rename to src/DAZ_Installer.Windows/Assets/favicon.ico diff --git a/Assets/loading.gif b/src/DAZ_Installer.Windows/Assets/loading.gif similarity index 100% rename from Assets/loading.gif rename to src/DAZ_Installer.Windows/Assets/loading.gif diff --git a/Assets/logo.png b/src/DAZ_Installer.Windows/Assets/logo.png similarity index 100% rename from Assets/logo.png rename to src/DAZ_Installer.Windows/Assets/logo.png diff --git a/Assets/thumb_14366704070ZIP.png b/src/DAZ_Installer.Windows/Assets/thumb_14366704070ZIP.png similarity index 100% rename from Assets/thumb_14366704070ZIP.png rename to src/DAZ_Installer.Windows/Assets/thumb_14366704070ZIP.png diff --git a/src/DAZ_Installer.Windows/DAZ_Installer.Windows.csproj b/src/DAZ_Installer.Windows/DAZ_Installer.Windows.csproj new file mode 100644 index 0000000..a4340d2 --- /dev/null +++ b/src/DAZ_Installer.Windows/DAZ_Installer.Windows.csproj @@ -0,0 +1,186 @@ + + + + WinExe + net6.0-windows + enable + true + true + disable + AnyCPU + Product Manager for Daz Studio + $(AssemblyVersion)$(VersionSuffix) + Product Manager for Daz Studio + Solomon Blount and the community + Product Manager for Daz Studio + Solomon Blount + 0.9.* + True + True + + $(AssemblyVersion)$(VersionSuffix) + 0.9.* + + + + False + 512 + Pre-alpha + True + + + + False + 512 + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + True + True + Settings.settings + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + DAZ_Installer.Windows + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/DAZ_Installer.Windows/DP/DPExtractJob.cs b/src/DAZ_Installer.Windows/DP/DPExtractJob.cs new file mode 100644 index 0000000..17255fe --- /dev/null +++ b/src/DAZ_Installer.Windows/DP/DPExtractJob.cs @@ -0,0 +1,295 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +using DAZ_Installer.Core; +using DAZ_Installer.Core.Extraction; +using DAZ_Installer.Database; +using DAZ_Installer.IO; +using DAZ_Installer.UI; +using DAZ_Installer.Windows.Pages; +using Microsoft.VisualBasic.FileIO; +using Serilog; +using Serilog.Core; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace DAZ_Installer.Windows.DP +{ + internal class DPExtractJob + { + public static ILogger Logger { get; set; } = Log.Logger.ForContext(); + public List FilesToProcess { get; init; } + public bool Completed { get; protected set; } = false; + public DPProcessor Processor = new(); + public Task TaskJob { get; protected set; } + public DPSettings UserSettings { get; protected set; } + + public static DPTaskManager extractJobs = new(); + private ProgressCombo progressCombo = Extract.ExtractPage.progressCombo; + public static Queue Jobs { get; } = new Queue(); + // TODO: Check if a product is already in list. + + //public DPExtractJob(string[] files) + //{ + // filesToProcess = files; + //} + + public DPExtractJob(ListView.ListViewItemCollection files) + { + Jobs.Enqueue(this); + var _files = new List(files.Count); + + foreach (ListViewItem file in files) + _files.Add(file.Text); + FilesToProcess = _files; + } + public Task DoJob() + { + TaskJob = extractJobs.AddToQueue(ProcessListAsync); + return TaskJob; + } + + private void SetupEventHandlers() + { + Processor.ArchiveEnter += Processor_ArchiveEnter; + Processor.ArchiveExit += Processor_ArchiveExit; + Processor.ProcessError += Processor_ProcessError; + Processor.StateChanged += Processor_StateChanged; + Processor.FileError += Processor_FileError; + Processor.ExtractProgress += Processor_ExtractProgress; + Processor.MoveProgress += Processor_MoveProgress; + } + + + + private void Processor_StateChanged() + { + if (Processor.State == ProcessorState.PreparingExtraction) + { + // TO DO: Highlight files in red for files that failed to extract. + Extract.ExtractPage.BeginInvoke(() => + { + Extract.ExtractPage.SuspendLayout(); + try + { + Extract.ExtractPage.AddToList(Processor.CurrentArchive); + Extract.ExtractPage.AddToHierachy(Processor.CurrentArchive); + progressCombo.ChangeProgressBarStyle(true); + progressCombo.SetText($"Preparing to extract contents in {Processor.CurrentArchive.FileName}..."); + progressCombo.SetProgress(0); + } catch (Exception ex) + { + Logger.Error(ex, "An error occurred while attempting to add archive to list"); + } finally + { + Extract.ExtractPage.ResumeLayout(); + } + }); + } + else if (Processor.State == ProcessorState.Analyzing) + { + progressCombo.ChangeProgressBarStyle(true); + progressCombo.SetText($"Analyzing file contents in {Processor.CurrentArchive.FileName}..."); + } + } + + private void Processor_ExtractProgress(object sender, DPExtractProgressArgs e) + { + progressCombo.ChangeProgressBarStyle(false); + progressCombo.SetProgress(e.ExtractionPercentage); + progressCombo.SetText($"Extracting contents from {e.Archive.FileName}...{e.ExtractionPercentage}%"); + } + + private void Processor_MoveProgress(DPProcessor sender, DPExtractProgressArgs e) + { + progressCombo.ChangeProgressBarStyle(true); + progressCombo.SetText($"Moving files from {e.Archive.FileName} to destination...%"); + } + + private void Processor_FileError(object sender, DPErrorArgs e) => + // TODO: Log error? + throw new NotImplementedException(); + + private void Processor_ProcessError(object sender, DPProcessorErrorArgs e) + { + MessageBox.Show("An unexpected error occurred while processing the archive.\n" + + $"Error: {e.Explaination}\n" + + $"Stack Trace: {e.Ex?.StackTrace}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + + private void Processor_ArchiveExit(object sender, DPArchiveExitArgs e) + { + // TODO: Do stuff w/ archive. + if (!e.Processed) return; + // Create records if applicable. + // TODO: Only add if successful extraction, and all files from temp were moved, and/or user didn't cancel operation. + progressCombo.ChangeProgressBarStyle(true); + Logger.Information("Creating records for {arc}", e.Archive.FileName); + progressCombo.SetText($"Creating records for {e.Archive.FileName}..."); + CreateRecords(e.Archive, e.Report!); + + switch (UserSettings.PermDeleteSource) + { + case SettingOptions.Yes: + RemoveSourceFiles(); + break; + case SettingOptions.Prompt: + DialogResult result = MessageBox.Show("Do you wish to PERMENATELY delete all of the source files regardless if it was extracted or not? This cannot be undone.", + "Delete soruce files", MessageBoxButtons.YesNo, MessageBoxIcon.Question); + if (result == DialogResult.Yes) RemoveSourceFiles(); + break; + } + } + + private void Processor_ArchiveEnter(object sender, DPArchiveEnterArgs e) + { + if (Program.Database.ArchiveFileNames.Contains(e.Archive.FileName)) + { + switch (UserSettings.InstallPrevProducts) + { + case SettingOptions.No: + Processor.CancelCurrentArchive(); + break; + case SettingOptions.Prompt: + DialogResult result = MessageBox.Show($"It seems that \"{e.Archive.FileName}\" was already processed. " + + $"Do you wish to continue processing this file?", "Archive already processed", + MessageBoxButtons.YesNo, MessageBoxIcon.Information); + if (result == DialogResult.No) Processor.CancelCurrentArchive(); + break; + } + } + } + + private void ProcessListAsync(CancellationToken t) + { + try + { + // Tell the progress combo we are beginning by enabling visibility of the progress bar and cancel button. + progressCombo.StartProgress(); + + // Register the cancellation token so we can cancel the process. + var token = progressCombo.CancellationTokenSource.Token; + token.Register(Processor.CancelProcessing); + + // Snapshot the settings and this will be what we use + // throughout the entire extraction process. + UserSettings = DPSettings.GetCopy(); + var processSettings = new DPProcessSettings + { + ContentFolders = UserSettings.CommonContentFolderNames, + ContentRedirectFolders = UserSettings.FolderRedirects, + DestinationPath = UserSettings.DestinationPath, + TempPath = UserSettings.TempDir, + InstallOption = UserSettings.HandleInstallation, + OverwriteFiles = UserSettings.OverwriteFiles == SettingOptions.Yes || + UserSettings.OverwriteFiles == SettingOptions.Prompt, + ForceFileToDest = new Dictionary(0), + }; + SetupEventHandlers(); + + var c = FilesToProcess.Count; + for (var i = 0; i < c; i++) + { + var x = FilesToProcess[i]; + int percentage = (int)((double)i / c * 100); + progressCombo.SetProgress(percentage); + progressCombo.SetText($"Processing archive {i + 1}/{c}: " + + $"{Path.GetFileName(x)}...({percentage}%)"); + Processor.ProcessArchive(x, processSettings); + } + + // Update the database after this run. + Program.Database.GetInstalledArchiveNamesQ(); + } catch (Exception ex) + { + Logger.Error(ex, "An error occurred while attempting to process archive list"); + } finally + { + progressCombo.SetText($"Finished processing archives"); + progressCombo.ChangeProgressBarStyle(false); + progressCombo.SetProgress(100); + progressCombo.EndProgress(); + + Completed = true; + Jobs.Dequeue(); + GC.Collect(); + } + + } + + public void RemoveSourceFiles() + { + var scopeSettings = new DPFileScopeSettings(FilesToProcess, Array.Empty(), false, true); + var fs = new DPFileSystem(scopeSettings); + + foreach (var file in FilesToProcess) + { + Exception? ex = null; + if (UserSettings.DeleteAction == RecycleOption.DeletePermanently) + fs.CreateFileInfo(file).TryAndFixDelete(out ex); + else + { + try + { + if (fs.Scope.IsFilePathWhitelisted(file)) + FileSystem.DeleteFile(file, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin); + } catch (Exception e) { ex = e; } + } + if (ex != null) + Logger.Error(ex, "Failed to delete source file: {file}", file); + } + } + + private DPProductRecord? CreateRecords(DPArchive arc, DPExtractionReport report) + { + if (arc.Type != ArchiveType.Product) return null; + var imageLocation = string.Empty; + + // Extraction Record successful folder/file paths will now be relative to their content folder (if any). + var successfulFiles = new List(arc.Contents.Count); + // Folders where a file was extracted underneath it. + // Ex: Content/Documents/a.txt was extracted, therefore "Documents" is added. + var foldersExtracted = new HashSet(arc.Contents.Count); + + // Add the paths relative to the content folder. + foreach (DPFile file in report.ExtractedFiles) + { + successfulFiles.Add(file.RelativePathToContentFolder!); + if (!string.IsNullOrWhiteSpace(file.RelativePathToContentFolder)) + foldersExtracted.Add(Path.GetDirectoryName(file.RelativePathToContentFolder)!); + } + var erroredFiles = report.ErroredFiles.Keys.Select(x => x.RelativePathToContentFolder!).ToArray(); + var workingExtractionRecord = new DPExtractionRecord(arc.FileName, + UserSettings.DestinationPath, + successfulFiles.ToArray(), + erroredFiles, + report.ErroredFiles.Values.ToArray(), + foldersExtracted.ToArray(), + 0); + + if (UserSettings.DownloadImages == SettingOptions.Yes) + imageLocation = DPNetwork.DownloadImage(workingExtractionRecord.ArchiveFileName); + else if (UserSettings.DownloadImages == SettingOptions.Prompt) + { + // TODO: Use more reliable method! Support files! + // Pre-check if the archive file name starts with "IM" + if (workingExtractionRecord.ArchiveFileName.StartsWith("IM")) + { + DialogResult result = MessageBox.Show("Do you wish to download the thumbnail for this product?", "Download Thumbnail Prompt", MessageBoxButtons.YesNo); + if (result == DialogResult.Yes) imageLocation = DPNetwork.DownloadImage(workingExtractionRecord.ArchiveFileName); + } + } + + var author = arc.ProductInfo.Authors.FirstOrDefault(null as string); + var workingProductRecord = new DPProductRecord(arc.ProductName, arc.ProductInfo.Tags.ToArray(), author, + null, DateTime.Now, imageLocation, 0, 0); + Program.Database.AddNewRecordEntry(workingProductRecord, workingExtractionRecord); + return workingProductRecord; + } + } +} diff --git a/src/DP/DPGlobal.cs b/src/DAZ_Installer.Windows/DP/DPGlobal.cs similarity index 82% rename from src/DP/DPGlobal.cs rename to src/DAZ_Installer.Windows/DP/DPGlobal.cs index 78f402d..2e5d214 100644 --- a/src/DP/DPGlobal.cs +++ b/src/DAZ_Installer.Windows/DP/DPGlobal.cs @@ -2,19 +2,17 @@ // You may find a full copy of this license at root project directory\LICENSE using System; -using System.Collections.Generic; using System.Windows.Forms; -using System.Threading; -namespace DAZ_Installer.DP +namespace DAZ_Installer.Windows.DP { - static class DPGlobal + public static class DPGlobal { internal static int mainThreadID = 0; public static bool appClosing { get; set; } = false; public static event Action AppClosing; public static bool isWindows11 = false; - + // TODO: Handle closing while database is active. public static void HandleAppClosing(FormClosingEventArgs e) { appClosing = true; diff --git a/src/DP/DPNetwork.cs b/src/DAZ_Installer.Windows/DP/DPNetwork.cs similarity index 71% rename from src/DP/DPNetwork.cs rename to src/DAZ_Installer.Windows/DP/DPNetwork.cs index 9b74a6a..8ba4506 100644 --- a/src/DP/DPNetwork.cs +++ b/src/DAZ_Installer.Windows/DP/DPNetwork.cs @@ -1,16 +1,16 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE +using HtmlAgilityPack; using System; -using System.Net; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; -using System.Threading.Tasks; using System.IO; -using HtmlAgilityPack; +using System.Net; +using System.Threading.Tasks; -namespace DAZ_Installer.DP +namespace DAZ_Installer.Windows.DP { internal static class DPNetwork { @@ -18,7 +18,7 @@ internal static class DPNetwork // IM[ID]-1_ProductName.zip, where ID = ProductID // http://docs.daz3d.com/doku.php/public/read_me/index/[ID]/start // Must be filename only. - internal static string DownloadImage(string fileName) + internal static string? DownloadImage(string fileName) { try { @@ -27,8 +27,8 @@ internal static string DownloadImage(string fileName) var ID = int.Parse(fileName[2..fileName.IndexOf('-')]); var link = $@"http://docs.daz3d.com/doku.php/public/read_me/index/{ID}/start"; var web = new HtmlWeb(); - var htmlDoc = web.Load(link); - var imgNode = htmlDoc.DocumentNode.SelectSingleNode("/html/body/div[1]/div/div[2]/div[2]/div/div/div/p[1]/a/img"); + HtmlDocument htmlDoc = web.Load(link); + HtmlNode imgNode = htmlDoc.DocumentNode.SelectSingleNode("/html/body/div[1]/div/div[2]/div[2]/div/div/div/p[1]/a/img"); if (imgNode == null) return null; var imgLink = imgNode.GetAttributeValue("src", ""); // imgNode is null WHEN PAGE IS NOT FOUND. var equalSignIndex = imgLink.IndexOf("media") + 6; // +6 = media (5) + equal sign (1) @@ -36,21 +36,19 @@ internal static string DownloadImage(string fileName) if (imgLink != "") { // Download image. - using (WebClient client = new WebClient()) - { - var imgFileName = Path.GetFileNameWithoutExtension(fileName) + Path.GetExtension(imgLink); - var downloadLocation = Path.Combine(DPProcessor.settingsToUse.thumbnailsPath, imgFileName); - Directory.CreateDirectory(Path.GetDirectoryName(downloadLocation)); - client.DownloadFile(new Uri(gcdnLink), downloadLocation); - Task.Run(() => _downscaleImage(downloadLocation)); - return downloadLocation; - } + using var client = new WebClient(); + var imgFileName = Path.GetFileNameWithoutExtension(fileName) + Path.GetExtension(imgLink); + var downloadLocation = Path.Combine(DPSettings.CurrentSettingsObject.ThumbnailsDir, imgFileName); + Directory.CreateDirectory(Path.GetDirectoryName(downloadLocation)); + client.DownloadFile(new Uri(gcdnLink), downloadLocation); + Task.Run(() => downscaleImage(downloadLocation)); + return downloadLocation; } } } catch (Exception e) { - DPCommon.WriteToLog($"Unable to download image. REASON: {e}"); + // DPCommon.WriteToLog($"Unable to download image. REASON: {e}"); } return null; } @@ -59,7 +57,7 @@ internal static string DownloadImage(string fileName) /// Downscales the image on disk provided by to a 256x256 thumbnail, if possible. /// /// The location of the image, cannot be null. Does accept invalid paths or paths without access. - private static void _downscaleImage(string downloadLocation) + private static void downscaleImage(string downloadLocation) { if (!Directory.Exists(Path.GetDirectoryName(downloadLocation)) || !File.Exists(downloadLocation)) return; @@ -83,21 +81,22 @@ private static void _downscaleImage(string downloadLocation) graphics.SmoothingMode = SmoothingMode.HighQuality; graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; } - graphics.DrawImage(img, new Rectangle(0,0,256,256)); + graphics.DrawImage(img, new Rectangle(0, 0, 256, 256)); var eParams = new EncoderParameters(1); eParams.Param[0] = new EncoderParameter(Encoder.Quality, 100L); img.Dispose(); newImg.Save(downloadLocation, jpgCodec, eParams); - } catch (Exception ex) + } + catch (Exception ex) { - DPCommon.WriteToLog($"DPNetwork was unable to downscale image. REASON: {ex}"); + // DPCommon.WriteToLog($"DPNetwork was unable to downscale image. REASON: {ex}"); } } static DPNetwork() { - foreach (var codec in ImageCodecInfo.GetImageEncoders()) + foreach (ImageCodecInfo codec in ImageCodecInfo.GetImageEncoders()) { if (codec.FormatID == ImageFormat.Jpeg.Guid) { diff --git a/src/DAZ_Installer.Windows/DP/DPRegistry.cs b/src/DAZ_Installer.Windows/DP/DPRegistry.cs new file mode 100644 index 0000000..ae3d4a5 --- /dev/null +++ b/src/DAZ_Installer.Windows/DP/DPRegistry.cs @@ -0,0 +1,63 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +using Microsoft.Win32; +using System; +using System.Collections.Generic; + +namespace DAZ_Installer.Windows.DP +{ + /// + /// This class is used to gather important registry values. + /// + internal static class DPRegistry + { + /// + /// The DAZ Content Directories. May be empty if none found. + /// + internal static string[] ContentDirectories { get; set; } = Array.Empty(); + /// + /// The application path to DAZ Studio. Value may be if not found. + /// + internal static string DazAppPath { get; private set; } = string.Empty; + + static DPRegistry() => Refresh(); + /// + /// Fetches the content directories from the registry. + /// + /// The parent registry subkey, example: SOFTWARE\DAZ\Studio4. + /// The content directories found from registry. + private static string[] GetContentDirectories(RegistryKey key) + { + var dirs = new List(); + for (byte i = 0; i < byte.MaxValue; i++) + { + var contentDirName = "ContentDir" + i.ToString(); + var contentDirVal = key.GetValue(contentDirName, string.Empty) as string; + if (string.IsNullOrEmpty(contentDirVal)) break; + dirs.Add(contentDirVal); + } + return dirs.ToArray(); + } + + /// + /// Updates DPRegistry values. + /// + internal static void Refresh() + { + RegistryKey DazStudioKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\DAZ\Studio4"); + if (DazStudioKey == null) return; + ContentDirectories = GetContentDirectories(DazStudioKey); + + // Get App Path. + var valueNames = DazStudioKey.GetValueNames(); + var installPathName = "InstallPath-64"; + foreach (var name in valueNames) + { + if (name.Contains("InstallPath")) installPathName = name; + } + DazAppPath = DazStudioKey.GetValue(installPathName, "") as string; + } + + } +} diff --git a/src/DAZ_Installer.Windows/DP/DPSettings.cs b/src/DAZ_Installer.Windows/DP/DPSettings.cs new file mode 100644 index 0000000..5c6094a --- /dev/null +++ b/src/DAZ_Installer.Windows/DP/DPSettings.cs @@ -0,0 +1,177 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +using DAZ_Installer.Core; + +using Microsoft.VisualBasic.FileIO; +using Newtonsoft.Json; +using Serilog; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace DAZ_Installer.Windows.DP +{ + public enum SettingOptions + { + No, Yes, Prompt + } + + /// + /// DPSettings is a data class that holds all the settings for the application. It is serialized to a JSON file. + /// + public class DPSettings + { + // Note: The JSONSerializer will not use the setter for HashSet, therefore the + [JsonIgnore] + public static DPSettings CurrentSettingsObject; + public static ILogger Logger { get; set; } = Log.Logger.ForContext(); + /// + /// Represents the DAZ content library (or destination) to install the products to. + /// + public string DestinationPath { get; set; } // todo : Ask for daz content directory if no detected daz content paths found. + // TO DO: Use HashSet instead of list. + public string[] detectedDazContentPaths; + /// + /// Determines whether to download thumbnail images of the product. + /// + public SettingOptions DownloadImages { get; set; } = SettingOptions.Prompt; + /// + /// The thumbnail directory to use for the application to use. + /// + public string ThumbnailsDir { get; set; } = "Thumbnails"; + /// + /// The install option to use as part of . It is used to determine what to do with the manifest and whether to auto install. + /// + public InstallOptions HandleInstallation { get; set; } = InstallOptions.ManifestAndAuto; + /// + /// The default common content folder names. These are used to determine if a folder is a common content folder. For example, if a folder is called "data", it will be considered a common content folder. + /// + /// + /// Common content folder names are used to determine if a folder is a common content folder. For example, if a folder is called "data", it will be considered a common content folder. + /// + public HashSet CommonContentFolderNames { get; set; } = new HashSet(DPProcessor.DefaultContentFolders, StringComparer.OrdinalIgnoreCase); + /// + /// Folder Redirects (or aliases) are used to redirect folders to another folder. For example, if you have a folder called "docs" in your DAZ content directory, you can redirect it to "Documentation" by adding "docs" to the key and "Documentation" to the value. + /// + public Dictionary FolderRedirects { get; set; } = new Dictionary(DPProcessor.DefaultRedirects, StringComparer.OrdinalIgnoreCase); + /// + /// The temporary directory to use for the application to use. + /// + public string TempDir { get; set; } = Path.Combine(Path.GetTempPath(), "DazProductInstaller"); + /// + /// Set the maximum tags to show on s. This should be kept low because GDI+ is slow. Default is set to 8. + /// + public uint MaxTagsToShow { get; set; } = 8; + /// + /// Determines whether to delete the source files after installation. + /// + public SettingOptions PermDeleteSource { get; set; } = SettingOptions.Prompt; + /// + /// Determines whether to install products that are already installed (or in the library/database). + /// + public SettingOptions InstallPrevProducts { get; set; } = SettingOptions.Prompt; + /// + /// Determines whether to overwrite files when installing products. + /// + public SettingOptions OverwriteFiles { get; set; } = SettingOptions.Yes; + /// + /// The delete action to use when deleting files from DAZ content directories. This does not apply to temp files. + /// + public RecycleOption DeleteAction { get; set; } = RecycleOption.DeletePermanently; + /// + /// The directory for the database to use. This is not the database file itself. + /// + public string DatabaseDir { get; set; } = "Database"; + /// + /// Determines whether the current settings are valid. It checks whtehr the directories exist and have access to them. + /// + public bool Valid => Verify(); + private const string SETTINGS_PATH = "settings.json"; + + static DPSettings() + { + var fileInfo = new FileInfo(SETTINGS_PATH); + CurrentSettingsObject = fileInfo.Exists ? FromJson(File.ReadAllText(SETTINGS_PATH)) ?? new DPSettings() : new DPSettings(); + } + public DPSettings() => detectedDazContentPaths = DPRegistry.ContentDirectories; + /// + /// Clones the current settings object and returns it. + /// + + public static DPSettings GetCopy() + { + var settings = (DPSettings)CurrentSettingsObject.MemberwiseClone(); + settings.CommonContentFolderNames = new HashSet(CurrentSettingsObject.CommonContentFolderNames, StringComparer.OrdinalIgnoreCase); + settings.FolderRedirects = new Dictionary(CurrentSettingsObject.FolderRedirects, StringComparer.OrdinalIgnoreCase); + settings.detectedDazContentPaths = (string[])CurrentSettingsObject.detectedDazContentPaths.Clone(); + return settings; + } + /// + /// Returns the parsed DPSettings object from the JSON string. Returns null if failed to parse. + /// + /// The JSON of a DPSettings object. + public static DPSettings? FromJson(string json) + { + try + { + return JsonConvert.DeserializeObject(json); + } + catch (Exception ex) + { + Logger.Error(ex, "Failed to parse settings"); + } + return null; + } + public string ToJson() => JsonConvert.SerializeObject(this, Formatting.Indented); + /// + /// Verifies whether the directories exist and have access to them. calls this function. + /// + /// Whether the current settings object is valid or not. + public bool Verify() => Directory.Exists(DestinationPath) && Directory.Exists(ThumbnailsDir) && Directory.Exists(TempDir) && Directory.Exists(DatabaseDir); + /// + /// Updates this settings object to default the directory values of , , and . + /// Note that default paths may not be valid (exist or have access to)."/> + /// + public void DefaultDirectories() + { + ThumbnailsDir = createDirectoryIfNotExists("Thumbnails"); + TempDir = createDirectoryIfNotExists(Path.Combine(Path.GetTempPath(), "DazProductInstaller")); + DatabaseDir = createDirectoryIfNotExists("Database"); + } + + /// + /// Creates the directory if it does not exist (or app does not have access to it). No exception is thrown if it fails. + /// + /// The path of the directory to create. + /// The passed in. + private string createDirectoryIfNotExists(string path) + { + if (!Directory.Exists(path)) + { + TryHelper.Try(() => Directory.CreateDirectory(path)); + } + return path; + } + /// + /// Resets this settings object to its default values. Note that default paths may not be valid (exist or have access to). + /// + public void Reset() + { + // Reset all properties to default values. + detectedDazContentPaths = DPRegistry.ContentDirectories; + HandleInstallation = InstallOptions.ManifestAndAuto; + CommonContentFolderNames = new HashSet(DPProcessor.DefaultContentFolders, StringComparer.OrdinalIgnoreCase); + FolderRedirects = new Dictionary(DPProcessor.DefaultRedirects, StringComparer.OrdinalIgnoreCase); + MaxTagsToShow = 8; + PermDeleteSource = SettingOptions.Prompt; + InstallPrevProducts = SettingOptions.Prompt; + OverwriteFiles = SettingOptions.Yes; + DeleteAction = RecycleOption.DeletePermanently; + DefaultDirectories(); + } + + + } +} diff --git a/src/DAZ_Installer.Windows/Forms/AboutForm.Designer.cs b/src/DAZ_Installer.Windows/Forms/AboutForm.Designer.cs new file mode 100644 index 0000000..36726aa --- /dev/null +++ b/src/DAZ_Installer.Windows/Forms/AboutForm.Designer.cs @@ -0,0 +1,115 @@ +namespace DAZ_Installer.Windows.Forms +{ + partial class AboutForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + var resources = new System.ComponentModel.ComponentResourceManager(typeof(AboutForm)); + titleLbl = new System.Windows.Forms.Label(); + pictureBox1 = new System.Windows.Forms.PictureBox(); + mainInfoLbl = new System.Windows.Forms.Label(); + licensesRichTxtBox = new System.Windows.Forms.RichTextBox(); + licensesLbl = new System.Windows.Forms.Label(); + ((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit(); + SuspendLayout(); + // + // titleLbl + // + titleLbl.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point); + titleLbl.Location = new System.Drawing.Point(12, 117); + titleLbl.Name = "titleLbl"; + titleLbl.Size = new System.Drawing.Size(313, 23); + titleLbl.TabIndex = 0; + titleLbl.Text = "Product Manager for DAZ Studio"; + titleLbl.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // pictureBox1 + // + pictureBox1.Image = Resources.Logo2_256x; + pictureBox1.Location = new System.Drawing.Point(84, 12); + pictureBox1.Name = "pictureBox1"; + pictureBox1.Size = new System.Drawing.Size(164, 102); + pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; + pictureBox1.TabIndex = 1; + pictureBox1.TabStop = false; + // + // mainInfoLbl + // + mainInfoLbl.Location = new System.Drawing.Point(12, 149); + mainInfoLbl.Name = "mainInfoLbl"; + mainInfoLbl.Size = new System.Drawing.Size(313, 110); + mainInfoLbl.TabIndex = 2; + mainInfoLbl.Text = "[TEXT DETERMINED AT FORM LOAD]"; + mainInfoLbl.TextAlign = System.Drawing.ContentAlignment.TopCenter; + // + // licensesRichTxtBox + // + licensesRichTxtBox.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + licensesRichTxtBox.Location = new System.Drawing.Point(12, 285); + licensesRichTxtBox.Name = "licensesRichTxtBox"; + licensesRichTxtBox.Size = new System.Drawing.Size(313, 176); + licensesRichTxtBox.TabIndex = 3; + licensesRichTxtBox.Text = "no u"; + // + // licensesLbl + // + licensesLbl.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point); + licensesLbl.Location = new System.Drawing.Point(12, 259); + licensesLbl.Name = "licensesLbl"; + licensesLbl.Size = new System.Drawing.Size(313, 23); + licensesLbl.TabIndex = 4; + licensesLbl.Text = "Licenses"; + licensesLbl.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // AboutForm + // + AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; + ClientSize = new System.Drawing.Size(337, 473); + Controls.Add(licensesLbl); + Controls.Add(licensesRichTxtBox); + Controls.Add(mainInfoLbl); + Controls.Add(pictureBox1); + Controls.Add(titleLbl); + Icon = (System.Drawing.Icon)resources.GetObject("$this.Icon"); + MaximumSize = new System.Drawing.Size(353, 9999); + MinimumSize = new System.Drawing.Size(353, 421); + Name = "AboutForm"; + Text = "About Form"; + ((System.ComponentModel.ISupportInitialize)pictureBox1).EndInit(); + ResumeLayout(false); + } + + #endregion + + private System.Windows.Forms.Label titleLbl; + private System.Windows.Forms.PictureBox pictureBox1; + private System.Windows.Forms.Label mainInfoLbl; + private System.Windows.Forms.RichTextBox licensesRichTxtBox; + private System.Windows.Forms.Label licensesLbl; + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.Windows/Forms/AboutForm.cs b/src/DAZ_Installer.Windows/Forms/AboutForm.cs new file mode 100644 index 0000000..90a12bf --- /dev/null +++ b/src/DAZ_Installer.Windows/Forms/AboutForm.cs @@ -0,0 +1,20 @@ +using System.Windows.Forms; + +namespace DAZ_Installer.Windows.Forms +{ + public partial class AboutForm : Form + { + public static string AboutString = + "Copyright © Solomon Blount" + "\n" + + $"{Program.AppName} {Program.AppVersion} {Program.VersionSuffix}" + "\n" + + "\n" + + $"{Program.AppName} is an application that allows users to install and manage their products for DAZ Studio from any vendor supporting common packaging formats. "; + + public AboutForm() + { + InitializeComponent(); + mainInfoLbl.Text = AboutString; + titleLbl.Text = Program.AppName; + } + } +} diff --git a/src/Forms/AboutForm.resx b/src/DAZ_Installer.Windows/Forms/AboutForm.resx similarity index 98% rename from src/Forms/AboutForm.resx rename to src/DAZ_Installer.Windows/Forms/AboutForm.resx index 0cfb99f..bfa892a 100644 --- a/src/Forms/AboutForm.resx +++ b/src/DAZ_Installer.Windows/Forms/AboutForm.resx @@ -1,4 +1,64 @@ - + + + @@ -57,13 +117,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Copyright © - Solomon Blount -Daz Product Installer Version 0.6 Pre-Alpha - -Daz Product Installer is an application that allows users to install and manage their products for DAZ Studio from any vendor supporting common packaging formats. - - diff --git a/src/Forms/ContentFolderAliasManager.Designer.cs b/src/DAZ_Installer.Windows/Forms/ContentFolderAliasManager.Designer.cs similarity index 99% rename from src/Forms/ContentFolderAliasManager.Designer.cs rename to src/DAZ_Installer.Windows/Forms/ContentFolderAliasManager.Designer.cs index a556f85..ca5941c 100644 --- a/src/Forms/ContentFolderAliasManager.Designer.cs +++ b/src/DAZ_Installer.Windows/Forms/ContentFolderAliasManager.Designer.cs @@ -1,4 +1,4 @@ -namespace DAZ_Installer.Forms +namespace DAZ_Installer.Windows.Forms { partial class ContentFolderAliasManager { diff --git a/src/Forms/ContentFolderAliasManager.cs b/src/DAZ_Installer.Windows/Forms/ContentFolderAliasManager.cs similarity index 76% rename from src/Forms/ContentFolderAliasManager.cs rename to src/DAZ_Installer.Windows/Forms/ContentFolderAliasManager.cs index 21d5836..0001b5d 100644 --- a/src/Forms/ContentFolderAliasManager.cs +++ b/src/DAZ_Installer.Windows/Forms/ContentFolderAliasManager.cs @@ -1,20 +1,16 @@ -using System; +using DAZ_Installer.Windows.DP; +using System; using System.Collections.Generic; using System.ComponentModel; -using System.Data; -using System.Drawing; using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows.Forms; -using DAZ_Installer.DP; -namespace DAZ_Installer.Forms +namespace DAZ_Installer.Windows.Forms { public partial class ContentFolderAliasManager : Form { - Dictionary Aliases = new Dictionary(DPSettings.currentSettingsObject.folderRedirects); - HashSet keys = new HashSet(DPSettings.currentSettingsObject.folderRedirects.Count, + Dictionary Aliases = new(DPSettings.CurrentSettingsObject.FolderRedirects); + HashSet keys = new(DPSettings.CurrentSettingsObject.FolderRedirects.Count, StringComparer.OrdinalIgnoreCase); public ListView AliasListView { get; init; } public ContentFolderAliasManager() @@ -32,32 +28,26 @@ public ContentFolderAliasManager() private void SetupComboBox() { contentFoldersComboBox.BeginUpdate(); - contentFoldersComboBox.Items.AddRange(DPSettings.currentSettingsObject.commonContentFolderNames.ToArray()); + contentFoldersComboBox.Items.AddRange(DPSettings.CurrentSettingsObject.CommonContentFolderNames.ToArray()); contentFoldersComboBox.AutoCompleteMode = AutoCompleteMode.SuggestAppend; contentFoldersComboBox.AutoCompleteSource = AutoCompleteSource.CustomSource; - contentFoldersComboBox.AutoCompleteCustomSource.AddRange(DPSettings.currentSettingsObject.commonContentFolderNames.ToArray()); + contentFoldersComboBox.AutoCompleteCustomSource.AddRange(DPSettings.CurrentSettingsObject.CommonContentFolderNames.ToArray()); contentFoldersComboBox.EndUpdate(); } private void SetupAliasList() { aliasListView.BeginUpdate(); - foreach (var pair in Aliases) + foreach (KeyValuePair pair in Aliases) { aliasListView.Items.Add($"{pair.Key} --> {pair.Value}"); } aliasListView.EndUpdate(); } - private void aliasListView_Resize(object _, EventArgs __) - { - aliasListView.Columns[0].Width = aliasListView.ClientSize.Width; - } + private void aliasListView_Resize(object _, EventArgs __) => aliasListView.Columns[0].Width = aliasListView.ClientSize.Width; - private void contextMenuStrip1_Opening(object _, CancelEventArgs __) - { - removeToolStripMenuItem.Enabled = aliasListView.SelectedItems.Count != 0; - } + private void contextMenuStrip1_Opening(object _, CancelEventArgs __) => removeToolStripMenuItem.Enabled = aliasListView.SelectedItems.Count != 0; private void addBtn_Click(object _, EventArgs __) { @@ -84,11 +74,11 @@ private void removeToolStripMenuItem_Click(object sender, EventArgs e) var toDeleteItems = new ListViewItem[aliasListView.SelectedItems.Count]; aliasListView.BeginUpdate(); aliasListView.SelectedItems.CopyTo(toDeleteItems, 0); - foreach (var item in toDeleteItems) + foreach (ListViewItem item in toDeleteItems) { var tokens = item.Text.Split(" --> "); var key = tokens[0]; - keys.TryGetValue(key, out string realKey); + keys.TryGetValue(key, out var realKey); Aliases.Remove(realKey); keys.Remove(realKey); aliasListView.Items.Remove(item); @@ -101,8 +91,8 @@ private void resetToSavedToolStripMenuItem_Click(object sender, EventArgs e) aliasListView.BeginUpdate(); aliasListView.Items.Clear(); Aliases.Clear(); - Aliases = new Dictionary(DPSettings.currentSettingsObject.folderRedirects); - foreach (var pair in Aliases) + Aliases = new Dictionary(DPSettings.CurrentSettingsObject.FolderRedirects); + foreach (KeyValuePair pair in Aliases) { aliasListView.Items.Add($"{pair.Key} --> {pair.Value}"); } diff --git a/src/Forms/ContentFolderAliasManager.resx b/src/DAZ_Installer.Windows/Forms/ContentFolderAliasManager.resx similarity index 100% rename from src/Forms/ContentFolderAliasManager.resx rename to src/DAZ_Installer.Windows/Forms/ContentFolderAliasManager.resx diff --git a/src/Forms/ContentFolderManager.Designer.cs b/src/DAZ_Installer.Windows/Forms/ContentFolderManager.Designer.cs similarity index 99% rename from src/Forms/ContentFolderManager.Designer.cs rename to src/DAZ_Installer.Windows/Forms/ContentFolderManager.Designer.cs index ba7ddd0..55b8a16 100644 --- a/src/Forms/ContentFolderManager.Designer.cs +++ b/src/DAZ_Installer.Windows/Forms/ContentFolderManager.Designer.cs @@ -1,4 +1,4 @@ -namespace DAZ_Installer.Forms +namespace DAZ_Installer.Windows.Forms { partial class ContentFolderManager { diff --git a/src/Forms/ContentFolderManager.cs b/src/DAZ_Installer.Windows/Forms/ContentFolderManager.cs similarity index 86% rename from src/Forms/ContentFolderManager.cs rename to src/DAZ_Installer.Windows/Forms/ContentFolderManager.cs index 4b7968d..dffc4b6 100644 --- a/src/Forms/ContentFolderManager.cs +++ b/src/DAZ_Installer.Windows/Forms/ContentFolderManager.cs @@ -1,21 +1,21 @@ -using System; +using DAZ_Installer.Windows.DP; +using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Text; using System.Windows.Forms; -using DAZ_Installer.DP; -namespace DAZ_Installer.Forms +namespace DAZ_Installer.Windows.Forms { public partial class ContentFolderManager : Form { - public static HashSet InvalidChars = new HashSet(Path.GetInvalidFileNameChars()); + public static HashSet InvalidChars = new(Path.GetInvalidFileNameChars()); public HashSet ContentFolders { get; } = new HashSet(StringComparer.OrdinalIgnoreCase); public ContentFolderManager() { InitializeComponent(); - ContentFolders.UnionWith(DPSettings.currentSettingsObject.commonContentFolderNames); + ContentFolders.UnionWith(DPSettings.CurrentSettingsObject.CommonContentFolderNames); SetupContentFoldersList(); contentFoldersView.Columns[0].Width = contentFoldersView.ClientSize.Width; } @@ -32,7 +32,7 @@ private void SetupContentFoldersList() private void listViewContextMenu_Opening(object sender, CancelEventArgs e) { - removeToolStripMenuItem.Enabled = copyToolStripMenuItem.Enabled = + removeToolStripMenuItem.Enabled = copyToolStripMenuItem.Enabled = contentFoldersView.SelectedIndices.Count != 0; } @@ -41,8 +41,8 @@ private void resetToDefaultToolStripMenuItem_Click(object sender, EventArgs e) contentFoldersView.BeginUpdate(); contentFoldersView.Items.Clear(); ContentFolders.Clear(); - ContentFolders.UnionWith(DPSettings.currentSettingsObject.commonContentFolderNames); - foreach(var item in ContentFolders) + ContentFolders.UnionWith(DPSettings.CurrentSettingsObject.CommonContentFolderNames); + foreach (var item in ContentFolders) { contentFoldersView.Items.Add(item); } @@ -50,10 +50,7 @@ private void resetToDefaultToolStripMenuItem_Click(object sender, EventArgs e) } // Used to update the width of the invisible column; without this items will be truncated. - private void contentFoldersView_Resize(object sender, EventArgs e) - { - contentFoldersView.Columns[0].Width = contentFoldersView.ClientSize.Width; - } + private void contentFoldersView_Resize(object sender, EventArgs e) => contentFoldersView.Columns[0].Width = contentFoldersView.ClientSize.Width; private void copyToolStripMenuItem_Click(object sender, EventArgs e) { @@ -63,7 +60,7 @@ private void copyToolStripMenuItem_Click(object sender, EventArgs e) Clipboard.SetText(contentFoldersView.SelectedItems[0].Text); return; } - StringBuilder builder = new StringBuilder(50); + var builder = new StringBuilder(50); for (var i = 0; i < contentFoldersView.SelectedItems.Count; i++) { builder.AppendLine(contentFoldersView.SelectedItems[i].Text); @@ -76,7 +73,7 @@ private void removeToolStripMenuItem_Click(object sender, EventArgs e) var toDeleteItems = new ListViewItem[contentFoldersView.SelectedItems.Count]; contentFoldersView.SelectedItems.CopyTo(toDeleteItems, 0); contentFoldersView.BeginUpdate(); - foreach (var item in toDeleteItems) + foreach (ListViewItem item in toDeleteItems) { contentFoldersView.Items.Remove(item); ContentFolders.Remove(item.Text); @@ -119,7 +116,7 @@ private void contentFoldersView_KeyDown(object sender, KeyEventArgs e) private void contentFoldersView_KeyPress(object sender, KeyPressEventArgs e) { - if (e.KeyChar == (char) Keys.Delete && contentFoldersView.SelectedItems.Count != 0) + if (e.KeyChar == (char)Keys.Delete && contentFoldersView.SelectedItems.Count != 0) removeToolStripMenuItem_Click(null, null); } diff --git a/src/Forms/ContentFolderManager.resx b/src/DAZ_Installer.Windows/Forms/ContentFolderManager.resx similarity index 100% rename from src/Forms/ContentFolderManager.resx rename to src/DAZ_Installer.Windows/Forms/ContentFolderManager.resx diff --git a/src/Forms/DatabaseView.Designer.cs b/src/DAZ_Installer.Windows/Forms/DatabaseView.Designer.cs similarity index 99% rename from src/Forms/DatabaseView.Designer.cs rename to src/DAZ_Installer.Windows/Forms/DatabaseView.Designer.cs index b56c36c..e141a0a 100644 --- a/src/Forms/DatabaseView.Designer.cs +++ b/src/DAZ_Installer.Windows/Forms/DatabaseView.Designer.cs @@ -1,4 +1,4 @@ -namespace DAZ_Installer.Forms +namespace DAZ_Installer.Windows.Forms { partial class DatabaseView { diff --git a/src/Forms/DatabaseView.cs b/src/DAZ_Installer.Windows/Forms/DatabaseView.cs similarity index 76% rename from src/Forms/DatabaseView.cs rename to src/DAZ_Installer.Windows/Forms/DatabaseView.cs index 922ed01..bbf1b59 100644 --- a/src/Forms/DatabaseView.cs +++ b/src/DAZ_Installer.Windows/Forms/DatabaseView.cs @@ -1,12 +1,12 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE -using DAZ_Installer.DP; +using DAZ_Installer.Windows.DP; using System; using System.Data; using System.Windows.Forms; -namespace DAZ_Installer.Forms +namespace DAZ_Installer.Windows.Forms { public partial class DatabaseView : Form { @@ -16,7 +16,7 @@ public partial class DatabaseView : Form public DatabaseView() { InitializeComponent(); - if (DPGlobal.isWindows11) changeTableBtn.Size = new System.Drawing.Size(changeTableBtn.Size.Width, + if (DPGlobal.isWindows11) changeTableBtn.Size = new System.Drawing.Size(changeTableBtn.Size.Width, changeTableBtn.Size.Height + 1); } @@ -34,10 +34,10 @@ public void ShowEverything(DataSet dataSet) private void DatabaseView_Load(object sender, EventArgs e) { - DP.DPDatabase.TableUpdated += OnTableChanged; - if (DP.DPDatabase.tableNames != null) + Program.Database.TableUpdated += OnTableChanged; + if (Program.Database.tableNames != null) { - tableNames.Items.AddRange(DP.DPDatabase.tableNames); + tableNames.Items.AddRange(Program.Database.tableNames); tableNames.SelectedIndex = 0; } } @@ -45,13 +45,13 @@ private void DatabaseView_Load(object sender, EventArgs e) private void changeTableBtn_Click(object sender, EventArgs e) { if (tableNames.Text.Trim().Length != 0) - DP.DPDatabase.ViewTableQ(tableNames.Text, 0, ShowEverything); + Program.Database.ViewTableQ(tableNames.Text, 0, ShowEverything); } private void OnTableChanged(string tableName) { if (tableName != tableNames.Text) return; - DP.DPDatabase.ViewTableQ(tableName, callback: ShowEverything); + Program.Database.ViewTableQ(tableName, callback: ShowEverything); } } } diff --git a/src/Forms/DatabaseView.resx b/src/DAZ_Installer.Windows/Forms/DatabaseView.resx similarity index 100% rename from src/Forms/DatabaseView.resx rename to src/DAZ_Installer.Windows/Forms/DatabaseView.resx diff --git a/src/DAZ_Installer.Windows/Forms/MainForm.Designer.cs b/src/DAZ_Installer.Windows/Forms/MainForm.Designer.cs new file mode 100644 index 0000000..a9c3bef --- /dev/null +++ b/src/DAZ_Installer.Windows/Forms/MainForm.Designer.cs @@ -0,0 +1,254 @@ +using DAZ_Installer.Windows.Pages; + +namespace DAZ_Installer.Windows.Forms +{ + partial class MainForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + var resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); + tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + pictureBox1 = new System.Windows.Forms.PictureBox(); + homeLabel = new System.Windows.Forms.Label(); + extractLbl = new System.Windows.Forms.Label(); + libraryLbl = new System.Windows.Forms.Label(); + settingsLbl = new System.Windows.Forms.Label(); + mainPanel = new System.Windows.Forms.Panel(); + homePage1 = new Home(); + extractControl1 = new Extract(); + library1 = new Library(); + settings1 = new Settings(); + openFileDialog = new System.Windows.Forms.OpenFileDialog(); + tableLayoutPanel1.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit(); + mainPanel.SuspendLayout(); + SuspendLayout(); + // + // tableLayoutPanel1 + // + tableLayoutPanel1.BackColor = System.Drawing.Color.FromArgb(53, 50, 56); + tableLayoutPanel1.ColumnCount = 1; + tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + tableLayoutPanel1.Controls.Add(pictureBox1, 0, 0); + tableLayoutPanel1.Controls.Add(homeLabel, 0, 1); + tableLayoutPanel1.Controls.Add(extractLbl, 0, 2); + tableLayoutPanel1.Controls.Add(libraryLbl, 0, 3); + tableLayoutPanel1.Controls.Add(settingsLbl, 0, 4); + tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Left; + tableLayoutPanel1.Location = new System.Drawing.Point(0, 0); + tableLayoutPanel1.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + tableLayoutPanel1.Name = "tableLayoutPanel1"; + tableLayoutPanel1.RowCount = 5; + tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F)); + tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F)); + tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F)); + tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F)); + tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F)); + tableLayoutPanel1.Size = new System.Drawing.Size(117, 344); + tableLayoutPanel1.TabIndex = 0; + // + // pictureBox1 + // + pictureBox1.BackColor = System.Drawing.Color.FromArgb(53, 50, 56); + pictureBox1.Dock = System.Windows.Forms.DockStyle.Fill; + pictureBox1.Image = Resources.Logo2_256x; + pictureBox1.Location = new System.Drawing.Point(3, 2); + pictureBox1.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + pictureBox1.Name = "pictureBox1"; + pictureBox1.Size = new System.Drawing.Size(111, 64); + pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; + pictureBox1.TabIndex = 0; + pictureBox1.TabStop = false; + pictureBox1.Click += pictureBox1_Click; + // + // homeLabel + // + homeLabel.AutoSize = true; + homeLabel.BackColor = System.Drawing.Color.FromArgb(53, 50, 56); + homeLabel.Dock = System.Windows.Forms.DockStyle.Fill; + homeLabel.Font = new System.Drawing.Font("Segoe UI Variable Text Light", 18F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + homeLabel.ForeColor = System.Drawing.Color.White; + homeLabel.Location = new System.Drawing.Point(3, 68); + homeLabel.Name = "homeLabel"; + homeLabel.Size = new System.Drawing.Size(111, 68); + homeLabel.TabIndex = 1; + homeLabel.Text = "Home"; + homeLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + homeLabel.Click += homeLabel_Click; + homeLabel.MouseEnter += sidePanelButtonMouseEnter; + homeLabel.MouseLeave += sidePanelButtonMouseExit; + // + // extractLbl + // + extractLbl.AutoSize = true; + extractLbl.Dock = System.Windows.Forms.DockStyle.Fill; + extractLbl.Font = new System.Drawing.Font("Segoe UI Variable Text Light", 18F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + extractLbl.ForeColor = System.Drawing.Color.White; + extractLbl.Location = new System.Drawing.Point(3, 136); + extractLbl.Name = "extractLbl"; + extractLbl.Size = new System.Drawing.Size(111, 68); + extractLbl.TabIndex = 2; + extractLbl.Text = "Extract"; + extractLbl.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + extractLbl.Click += extractLbl_Click; + extractLbl.MouseEnter += sidePanelButtonMouseEnter; + extractLbl.MouseLeave += sidePanelButtonMouseExit; + // + // libraryLbl + // + libraryLbl.AutoSize = true; + libraryLbl.Dock = System.Windows.Forms.DockStyle.Fill; + libraryLbl.Font = new System.Drawing.Font("Segoe UI Variable Text Light", 18F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + libraryLbl.ForeColor = System.Drawing.Color.White; + libraryLbl.Location = new System.Drawing.Point(3, 204); + libraryLbl.Name = "libraryLbl"; + libraryLbl.Size = new System.Drawing.Size(111, 68); + libraryLbl.TabIndex = 3; + libraryLbl.Text = "Library"; + libraryLbl.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + libraryLbl.Click += libraryLbl_Click; + libraryLbl.MouseEnter += sidePanelButtonMouseEnter; + libraryLbl.MouseLeave += sidePanelButtonMouseExit; + // + // settingsLbl + // + settingsLbl.AutoSize = true; + settingsLbl.Dock = System.Windows.Forms.DockStyle.Fill; + settingsLbl.Font = new System.Drawing.Font("Segoe UI Variable Text Light", 18F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + settingsLbl.ForeColor = System.Drawing.Color.White; + settingsLbl.Location = new System.Drawing.Point(3, 272); + settingsLbl.Name = "settingsLbl"; + settingsLbl.Size = new System.Drawing.Size(111, 72); + settingsLbl.TabIndex = 4; + settingsLbl.Text = "Settings"; + settingsLbl.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + settingsLbl.Click += settingsLbl_Click; + settingsLbl.MouseEnter += sidePanelButtonMouseEnter; + settingsLbl.MouseLeave += sidePanelButtonMouseExit; + // + // mainPanel + // + mainPanel.Controls.Add(homePage1); + mainPanel.Controls.Add(extractControl1); + mainPanel.Controls.Add(library1); + mainPanel.Controls.Add(settings1); + mainPanel.Dock = System.Windows.Forms.DockStyle.Fill; + mainPanel.Location = new System.Drawing.Point(117, 0); + mainPanel.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + mainPanel.Name = "mainPanel"; + mainPanel.Size = new System.Drawing.Size(542, 344); + mainPanel.TabIndex = 1; + // + // homePage1 + // + homePage1.AllowDrop = true; + homePage1.AutoSize = true; + homePage1.BackColor = System.Drawing.Color.White; + homePage1.Dock = System.Windows.Forms.DockStyle.Fill; + homePage1.Location = new System.Drawing.Point(0, 0); + homePage1.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + homePage1.MinimumSize = new System.Drawing.Size(494, 294); + homePage1.Name = "homePage1"; + homePage1.Size = new System.Drawing.Size(542, 344); + homePage1.TabIndex = 0; + // + // extractControl1 + // + extractControl1.BackColor = System.Drawing.Color.White; + extractControl1.Dock = System.Windows.Forms.DockStyle.Fill; + extractControl1.Location = new System.Drawing.Point(0, 0); + extractControl1.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + extractControl1.Name = "extractControl1"; + extractControl1.Size = new System.Drawing.Size(542, 344); + extractControl1.TabIndex = 1; + // + // library1 + // + library1.BackColor = System.Drawing.Color.White; + library1.Dock = System.Windows.Forms.DockStyle.Fill; + library1.Location = new System.Drawing.Point(0, 0); + library1.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + library1.Name = "library1"; + library1.Size = new System.Drawing.Size(542, 344); + library1.TabIndex = 2; + // + // settings1 + // + settings1.BackColor = System.Drawing.Color.FromArgb(192, 255, 192); + settings1.Dock = System.Windows.Forms.DockStyle.Fill; + settings1.Location = new System.Drawing.Point(0, 0); + settings1.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + settings1.Name = "settings1"; + settings1.Size = new System.Drawing.Size(542, 344); + settings1.TabIndex = 2; + // + // openFileDialog + // + openFileDialog.FileName = "openFileDialog1"; + openFileDialog.SupportMultiDottedExtensions = true; + // + // MainForm + // + AllowDrop = true; + AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; + ClientSize = new System.Drawing.Size(659, 344); + Controls.Add(mainPanel); + Controls.Add(tableLayoutPanel1); + Icon = (System.Drawing.Icon)resources.GetObject("$this.Icon"); + Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + MinimumSize = new System.Drawing.Size(675, 383); + Name = "MainForm"; + Text = "Product Manager for DAZ Studio"; + FormClosing += Form1_FormClosing; + Load += Form1_Load; + DragDrop += MainForm_DragDrop; + DragEnter += MainForm_DragEnter; + tableLayoutPanel1.ResumeLayout(false); + tableLayoutPanel1.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)pictureBox1).EndInit(); + mainPanel.ResumeLayout(false); + mainPanel.PerformLayout(); + ResumeLayout(false); + } + + #endregion + private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; + private System.Windows.Forms.PictureBox pictureBox1; + private System.Windows.Forms.Label homeLabel; + private System.Windows.Forms.Panel mainPanel; + private Home homePage1; + private System.Windows.Forms.Label extractLbl; + private System.Windows.Forms.Label libraryLbl; + private System.Windows.Forms.Label settingsLbl; + public Extract extractControl1; + private Library library1; + private Settings settings1; + internal System.Windows.Forms.OpenFileDialog openFileDialog; + } +} + diff --git a/src/Forms/MainForm.cs b/src/DAZ_Installer.Windows/Forms/MainForm.cs similarity index 70% rename from src/Forms/MainForm.cs rename to src/DAZ_Installer.Windows/Forms/MainForm.cs index ae63046..a40c10e 100644 --- a/src/Forms/MainForm.cs +++ b/src/DAZ_Installer.Windows/Forms/MainForm.cs @@ -1,13 +1,12 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE -using DAZ_Installer.DP; -using DAZ_Installer.Forms; +using DAZ_Installer.Windows.DP; using System; using System.Drawing; using System.Windows.Forms; -namespace DAZ_Installer +namespace DAZ_Installer.Windows.Forms { public partial class MainForm : Form { @@ -23,7 +22,7 @@ public MainForm() InitializeComponent(); activeForm = this; InitalizePages(); - + Text = Program.AppName; if (Environment.OSVersion.Version.Build >= 22000) DPGlobal.isWindows11 = true; } @@ -36,7 +35,7 @@ private void InitalizePages() userControls[2] = library1; userControls[3] = settings1; - foreach (var control in userControls) + foreach (UserControl control in userControls) { if (control == visiblePage) continue; control.Visible = false; @@ -57,37 +56,25 @@ internal static void SwitchPage(UserControl switchTo) visiblePage = switchTo; } - public void SwitchToExtractPage() - { - extractControl1.BringToFront(); - } + public void SwitchToExtractPage() => extractControl1.BringToFront(); - private void Form1_Load(object sender, EventArgs e) - { - initialSidePanelColor = tableLayoutPanel1.BackColor; - } + private void Form1_Load(object sender, EventArgs e) => initialSidePanelColor = tableLayoutPanel1.BackColor; private void sidePanelButtonMouseEnter(object sender, EventArgs e) { - Label button = (Label)sender; + var button = (Label)sender; button.ForeColor = Color.FromKnownColor(KnownColor.Coral); } private void sidePanelButtonMouseExit(object sender, EventArgs e) { - Label button = (Label)sender; + var button = (Label)sender; button.ForeColor = Color.FromKnownColor(KnownColor.White); } - private void extractLbl_Click(object sender, EventArgs e) - { - SwitchPage(extractControl1); - } + private void extractLbl_Click(object sender, EventArgs e) => SwitchPage(extractControl1); - private void homeLabel_Click(object sender, EventArgs e) - { - SwitchPage(homePage1); - } + private void homeLabel_Click(object sender, EventArgs e) => SwitchPage(homePage1); private void libraryLbl_Click(object sender, EventArgs e) { @@ -99,19 +86,15 @@ private void libraryLbl_Click(object sender, EventArgs e) } } - private void settingsLbl_Click(object sender, EventArgs e) - { - SwitchPage(settings1); - } + private void settingsLbl_Click(object sender, EventArgs e) => SwitchPage(settings1); - private void Form1_FormClosing(object sender, FormClosingEventArgs e) - { - DPGlobal.HandleAppClosing(e); - } + private void Form1_FormClosing(object sender, FormClosingEventArgs e) => DPGlobal.HandleAppClosing(e); - internal string? ShowMissingVolumePrompt(string msg, string filter, string ext, string? defaultLocation) { - var result = MessageBox.Show(msg, "Missing volumes", MessageBoxButtons.YesNo, MessageBoxIcon.Question); - if (result == DialogResult.Yes) { + internal string? ShowMissingVolumePrompt(string msg, string filter, string ext, string? defaultLocation) + { + DialogResult result = MessageBox.Show(msg, "Missing volumes", MessageBoxButtons.YesNo, MessageBoxIcon.Question); + if (result == DialogResult.Yes) + { return ShowFileDialog(filter, ext, defaultLocation); } return null; @@ -122,7 +105,7 @@ private void Form1_FormClosing(object sender, FormClosingEventArgs e) { if (InvokeRequired) { - return (string) Invoke(ShowFileDialog,filter, defaultExt, defaultLocation); + return (string)Invoke(ShowFileDialog, filter, defaultExt, defaultLocation); } openFileDialog.Filter = filter; openFileDialog.DefaultExt = defaultExt; @@ -130,7 +113,7 @@ private void Form1_FormClosing(object sender, FormClosingEventArgs e) { openFileDialog.InitialDirectory = defaultLocation; } - var result = openFileDialog.ShowDialog(); + DialogResult result = openFileDialog.ShowDialog(); if (result == DialogResult.OK) { return openFileDialog.FileName; @@ -138,9 +121,21 @@ private void Form1_FormClosing(object sender, FormClosingEventArgs e) return null; } - private void pictureBox1_Click(object sender, EventArgs e) + private void pictureBox1_Click(object sender, EventArgs e) => new AboutForm().ShowDialog(); + + private void MainForm_DragDrop(object sender, DragEventArgs e) { - new AboutForm().ShowDialog(); + + } + + private void MainForm_DragEnter(object sender, DragEventArgs e) + { + e.Effect = Program.DropEffect; + // Get the page we are currently in... If we are not on the home page, then switch to it. + if (visiblePage != homePage1) + { + SwitchPage(homePage1); + } } } } diff --git a/src/DAZ_Installer.Windows/Forms/MainForm.resx b/src/DAZ_Installer.Windows/Forms/MainForm.resx new file mode 100644 index 0000000..41d2d96 --- /dev/null +++ b/src/DAZ_Installer.Windows/Forms/MainForm.resx @@ -0,0 +1,1891 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + + + + AAABAAYAEBAAAAEAIABoBAAAZgAAACAgAAABACAAqBAAAM4EAAAwMAAAAQAgAKglAAB2FQAAQEAAAAEA + IAAoQgAAHjsAAICAAAABACAAKAgBAEZ9AAAAAAAAAQAgALoXAABuhQEAKAAAABAAAAAgAAAAAQAgAAAA + AAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhP0AAIf+AACH/gkAiv4/AIv+PACI + /goAif4AAIX+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAff0AAIH9AAB//QYAhf5EAIr+vwCL + /r0Ahv5HAIH9CACD/QAAfv0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAdvwAAHr9AAB4/QYAf/1DAIT9vgCJ + /v4Aiv7+AIb+vACA/UQAev0IAHz9AAB3/QAAAAAAAAAAAAAAAAAAbfwAAHL8AABw/AYAd/1DAH79vgCE + /f4Aif7/AIr+/wCF/v4AgP29AHn9RwBy/AgAdPwAAG78AAAAAAAAZfsAAGr8AABn/AYAb/xDAHf9vgB+ + /f4AhP3/AIn+3wCK/t8Ahf7/AH/9/gB4/cQAcPxKAGn8CABr/AAAZfsAAGH7AABe+wYAZvtDAG78vgB2 + /P4Afv3/AIT92QCI/j0Aif49AIX92QB//f8Ad/3/AHD8xQBo+0oAYPsIAGL7AABU+gMAXvtCAGb7vwBu + /P4Advz/AH392QCC/T4AfPwAAH38AACD/T4Afv3ZAHf9/wBv/P8AZ/vGAF/7SQBW+gQAjP8AAF/7UwBl + +/AAbvz/AHb83gB7/UIAdP0AAID9AACA/QAAdP0AAHz9PgB2/NgAb/z/AGb79ABg+14ABvUAAGL7AABc + +gQAaPtwAG780wB0/EcAYPkAAHn9AAAAAAAAAAAAAHn9AABr+wAAdPw5AG78zABo+3sAXvoGAGL7AABk + +wAAbvwAAGn8RQBt/J8Ac/wSAHL8AAAAAAAAAAAAAAAAAAB0/QAAcvwAAHP8GgBu/KoAafxJAHH9AABj + +wAAYvsAAGD7NQBm+9oAbvz+AHX8nAB8/RMAev0AAH/9AAB+/QAAev0AAHv9GQB1/K4Abvz/AGb73gBg + +zoAYfsAAGL7AABg+zAAZvvTAG78/wB3/f4Aff2kAIP9FQCB/QAAgf0AAIL9FQB9/agAdv3/AG78/wBm + +9IAYPsuAGH7AABk+wAAbv0AAGn8NQBv/NEAd/3/AH/9/gCE/aUAif4UAIj+FACE/aUAfv3+AHb8/wBv + /NEAaPw1AG78AABj+wAAAAAAAG38AAB3/AAAcvw1AHj90AB//f8Ahf7/AIn+mwCJ/psAhP3/AH79/wB3 + /dEAcfw1AHb8AABs/AAAAAAAAAAAAAAAAAAAdfwAAH/9AAB6/S8Af/3KAIX+6wCJ/lwAif5cAIT96gB+ + /dUAef02AH7+AAB0/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+/QAAhP0AAIL9NQCE/VYAi/8CAIz/AgCD + /VQAgP1CAIb9AAB8/QAAAAAAAAAAAAAAAAD8PwAA+B8AAPAPAADgBwAAwAMAAIABAAABgAAAg8EAAIfh + AADH4wAAg8EAAIGBAADAAwAA4AcAAPAPAAD4HwAAKAAAACAAAABAAAAAAQAgAAAAAAAAEAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAif4AAIn+EACK/iUAi/4fAIv+EQCL/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAIf+AACH/g0AiP5TAIr+YgCL/lQAiv5TAIj+EQCI/gAAh/4AAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAACE/QAAhP0NAIb+UACI/mUAi/7ZAIv+1QCK/l8Ah/5YAIX+FACG/gAAhP0AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAgf0AAIH9DQCD/U4Ahv5fAIj+0wCL/v8Ai/7/AIr+0wCH/lwAhP1WAIL9FACD + /QAAgf0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAH79AAB9/Q0Af/1OAIP9XwCG/tMAiP7/AIv+/wCL/v8Aif7/AIf+0wCE + /VcAgf1SAH/9FAB//QAAff0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB6/QAAef0NAHz9TgB//V8Ag/3TAIX+/wCI/v8Ai/7/AIv+/wCJ + /v8Ah/7/AIT90wCB/VUAff1RAHv9FAB8/QAAef0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdvwAAHb8DQB4/U4AfP1fAH/90wCD/f8Ahv7/AIj+/wCL + /v8Ai/7/AIn+/wCH/v8AhP3/AIH91AB+/VkAev1SAHf9FAB4/QAAdfwAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHL8AABx/A0AdPxOAHj9XwB8/dMAf/3/AIP9/wCG + /v8AiP7/AIv+/wCL/v8Aif7/AIf+/wCE/f8Agf3/AH392AB6/WAAdvxSAHP8FAB0/AAAcfwAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABu/AAAbfwNAHD8TgB0/F8AeP3TAHz9/wB/ + /f8Ag/3/AIb+/wCI/v8Ai/7+AIv+/gCJ/v8Ah/7/AIT9/wCB/f8Aff3/AHr93QB2/GUAcfxSAG/8FABv + /AAAbfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAafwAAGn8DQBs/E4AcPxfAHT80wB4 + /f8AfP3/AH/9/wCD/f8Ahv7/AIj+/QCK/qQAi/6kAIn+/QCH/v8AhP3/AIH9/wB9/f8Aef3/AHb84ABx + /GcAbfxSAGr8FABr/AAAaPsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGX7AABl+w0AZ/tOAGz8XwBw + /NMAdPz/AHj9/wB8/f8Af/3/AIP9/wCG/v0AiP6fAIr+EQCL/hEAif6fAIf+/QCE/f8Agf3/AH39/wB5 + /f8Adfz/AHH84ABt/GcAaPxSAGb7FABm+wAAZPsAAAAAAAAAAAAAAAAAAAAAAABh+wAAYPsNAGP7TgBn + +18AbPzTAHD8/wB0/P8AeP3/AHz9/wB//f8Ag/39AIX+nwCI/hIAh/4AAIj+AACI/hIAhv6fAIT9/QCA + /f8Aff3/AHn9/wB1/P8Acfz/AG384ABp/GcAZPtSAGH7FABi+wAAX/sAAAAAAAAAAAAAXPsAAFz7DQBf + +04AY/tfAGj70wBs/P8AcPz/AHT8/wB4/f8AfP3/AID9/QCC/Z8Ahf0SAIT9AAAAAAAAAAAAAIX+AACG + /hIAg/2fAID9/QB9/f8Aef3/AHX8/wBx/P8Abfz/AGn84ABk+2cAX/tSAF37FABd+wAAW/sAAFn6AABY + +gUAWvtKAF77YABj+9MAZ/v/AGz8/wBw/P8AdPz/AHj9/wB8/f4Af/2hAIL9EgCB/QAAAAAAAAAAAAAA + AAAAAAAAAIL9AACD/RIAgP2fAH39/QB5/f8Adfz/AHH8/wBt/P8AaPv/AGT74ABg+2gAW/tQAFn6CwBZ + +gAAWfoAAFf6AwBb+yQAX/vAAGP7/wBn+/8AbPz/AHD8/wB0/P8AeP3/AHv9qAB+/RQAfv0AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAH79AAB//RIAfP2fAHn9/QB1/P8Acfz/AGz8/wBo+/8AZPv/AGD71ABc + +zEAV/oFAFn6AAAAAAAAXfsAAFj7AgBg+2EAY/vtAGf7/wBs/P8AcPz/AHT8/wB4/bEAe/0ZAHr9AAB8 + /QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHv9AAB8/RIAeP2dAHX8/QBx/P8AbPz/AGj7/wBk + +/QAYPt2AFv7BgBd+wAAAAAAAAAAAAAAAAAAYvsAAFv7AgBk+2AAaPvtAGz8/wBw/P8AdPyzAHf9HAB2 + /AAAef0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHf9AAB4/RAAdPyWAHH8+wBs + /P8AaPv0AGX7dQBg+wUAYvsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZvsAAGD5AgBp/F8AbPzrAHD8swBz + /B0AcvwAAHX8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHL8AABz + /A0AcPyNAGz87gBp/HQAZPoFAGb7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAavwAAGr8FwBs + /K4Abvw9AG78AABx/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAcfwAAG38AABv/EcAbfy6AGr8GwBq/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGb7AABl + +xIAaPyfAGz8/QBw/LIAc/wdAHL8AAB1/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAHX8AABw/AAAc/w5AHD81QBs/P4AaPyhAGX7FgBm+wAAY/sAAAAAAAAAAAAAAAAAAAAAAABh + +wAAYPsSAGT7nwBo+/0AbPz/AHH8/wB0/LQAd/0eAHb8AAB5/QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAB5/QAAdPwAAHf9OQB0/NUAcPz/AGz8/wBo+/4AZPuvAGD7HABh+wAAXvsAAAAAAAAA + AAAAXvsAAFz7CQBg+5sAY/v+AGj7/wBs/P8Acfz/AHX8/wB4/bsAe/0nAHr9AAB9/QAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAff0AAHn9AAB7/TgAeP3VAHX8/wBw/P8AbPz/AGj7/wBj+/8AX/urAFz7DABd + +wAAAAAAAAAAAABe+wAAXPsHAGD7igBj+/oAaPv/AGz8/wBx/P8Adfz/AHn9/wB8/cUAf/0qAH79AACB + /QAAAAAAAAAAAAAAAAAAAAAAAID9AAB9/QAAfv0uAHz90AB5/f8AdPz/AHD8/wBs/P8AZ/v/AGP7+QBg + +30AW/sFAF37AAAAAAAAAAAAAAAAAABi+wAAYPsLAGT7igBo/PoAbPz/AHH8/wB1/P8Aef3/AH39/wCA + /cYAgv0qAIH9AACE/QAAAAAAAAAAAACE/QAAgf0AAIL9KgB//cYAfP3/AHj9/wB0/P8AcPz/AGz8/wBn + +/oAZPuJAGD7CQBh+wAAAAAAAAAAAAAAAAAAAAAAAAAAAABm+wAAZfsLAGn8igBs/PoAcfz/AHX8/wB5 + /f8Aff3/AID9/wCD/cYAhf4qAIT9AACH/gAAhv4AAIT9AACF/SoAg/3GAID9/wB8/f8AeP3/AHT8/wBw + /P8AbPz6AGj7igBk+wsAZvsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABr/AAAavwLAG38igBx + /PoAdfz/AHn9/wB9/f8AgP3/AIT9/wCG/sYAiP4qAIf+AACH/gAAiP4qAIb+xgCD/f8AgP3/AHz9/wB4 + /f8AdPz/AHD8+gBt/IoAafwLAGr8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABv + /AAAbvwLAHL8igB1/PoAef3/AH39/wCA/f8AhP3/AIf+/wCJ/sUAi/4qAIr+KgCI/sUAhv7/AIP9/wCA + /f8AfP3/AHj9/wB0/PoAcfyKAG38CwBu/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAABz/AAAcvwLAHb8iAB5/fgAff3/AID9/wCE/f8Ah/7/AIn+/wCL/qkAiv6pAIn+/wCG + /v8Ag/3/AH/9/wB8/f8AeP36AHX8igBx/AsAcvwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB3/QAAdv0IAHr9eQB9/fQAgP3/AIT9/wCH/v8Aif7VAIv+OgCK + /joAiP7VAIb+/wCD/f8Af/3/AHz9+gB5/YsAdfwLAHb8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB7/QAAef0FAH79dQCB/fQAhP3/AIb+1QCI + /jkAhv4AAIb+AACI/jkAhv7UAIP9/wB//f0AfP2VAHn9DAB6/QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//QAAfv0FAIH9dQCE + /dAAhv49AIP9AACI/gAAhv4AAIL8AACF/TIAgv3GAID9nwB9/REAfv0AAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACD + /QAAg/0JAIP9JACF/QIAhP0AAAAAAAAAAAAAg/0AAID9AACC/SAAgf0UAIH9AAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//D////gf///wD///4Af//8AD//+AAf//AAD//gAAf/wA + AD/4AAAf8AAAD+ABgAfAA8ADgAfgAYAP8AHAH/gD4D/8B/B//g/4//8f8H/+D+A//AfAH/gDwA/wA+AH + 4AfwA8AP+AGAH/wAAD/+AAB//wAA//+Bgf//w8P//+Pn/ygAAAAwAAAAYAAAAAEAIAAAAAAAACQAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiv4AAIr+CwCK/g4AjP4KAIv+CwCL + /gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACJ/gAAif4OAIn+cQCK + /jUAi/4jAIv+bgCK/hEAiv4AAIn+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIf+AACH + /g8AiP50AIn+VgCL/nAAjP5hAIr+RgCJ/noAiP4VAIj+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAhf4AAIX9DwCG/nMAh/5MAIr+bACL/vQAjP7wAIv+YwCI/kQAiP6AAIb+GQCH/gAAhf4AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + 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 + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgf0AAIL9AACC/SgAg/2XAIT9LQCE/QAAhf4AAAAAAAAA + AAAAAAAAAAAAAACE/QAAg/0AAIP9EwCC/Y0Agf1bAHv9AQB//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIP9AACD/QAAg/0GAIT9AQCE + /QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIH9AACC/QUAgf0CAIH9AAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///w///8AAP// + +B///wAA///wD///AAD//+AH//8AAP//wAP//wAA//+AAf//AAD//wAA//8AAP/+AAB//wAA//wAAD// + AAD/+AAAH/8AAP/wAAAP/wAA/+AAAAf/AAD/wAAAA/8AAP+AAAAB/wAA/wAAAAD/AAD+AAAAAH8AAPwA + AYAAPwAA+AADwAAfAADwAAfgAA8AAOAAD/AABwAAwAAf+AADAADAAD/8AAMAAPAAf/4ABwAA+AD//wAP + AAD8Af//gB8AAP4D///gPwAA/wf///B/AAD/D///8P8AAP4P///gfwAA/Af//8A/AAD4A///gB8AAPAB + //8ADwAA8AD//gAPAADwAH/+AA8AAPgAP/wAHwAA/AAf+AA/AAD+AA/wAH8AAP8AB+AA/wAA/4ADwAH/ + AAD/wAGAA/8AAP/gAAAH/wAA//AAAA//AAD/+AAAH/8AAP/8AYA//wAA//4DwH//AAD//wfgf/8AAP// + j/D//wAA///P+f//AAAoAAAAQAAAAIAAAAABACAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiv4AAIr+AgCK + /gEAjP4AAIv+AQCI/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAif4AAIn+DgCK/nAAiv4jAIz+EACL/mcAi/4TAIv+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAiP4AAIj+DwCJ/ooAiv6AAIv+FwCM/gwAi/5oAIr+jwCJ/hQAiv4AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh/4AAIf+DwCI/osAif53AIr+HgCL/qcAjP6TAIz+EACK + /mgAif6WAIj+GQCI/gAAiP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhv4AAIX+EACG/osAh/5wAIn+FwCK + /qEAi/7/AIz+/ACL/pQAiv4PAIj+ZQCI/p0Ah/4eAIf+AACG/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhP0AAIT9EACF + /YoAhv5rAIj+EgCJ/poAiv79AIv+/wCM/v8Ai/78AIr+lACJ/g8Ah/5gAIb+oQCF/iIAhv4AAIX9AAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAg/0AAIL9EACD/YkAhP1oAIf+EACI/pUAif78AIr+/wCL/v8AjP7/AIv+/wCK/vwAif6VAIj+DgCG + /lgAhf6iAIT9JQCE/QAAg/0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAgf0AAIH9EACC/YkAg/1nAIX+EACG/pQAh/78AIn+/wCK/v8Ai/7/AIz+/wCL + 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 + AAAAAAAAAAAAAAAAAAAAf/0AAH79AwCA/WoAgf3xAIP9/wCF/f0Ahv6ZAIf+DgCH/gAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAhv4AAIb+DACF/YMAg/31AIL9/wCA/f8Af/28AH39IQB+/QAAfP0AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACB/QAAgP0DAIL9agCD/fEAhP2rAIX+FACF + /gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACE/QAAhf0GAIP9cQCC/fIAgP2+AH/9IwCA + /QAAfv0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIL9AACC + /QUAg/1LAIT9HwCE/QAAhf0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIL9AACD + /QUAgv1OAIH9JACB/QAAgP0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP// + //y/////////+B/////////wD////////+AH////////wAP///////+AAf///////wAA///////+AAB/ + //////wAAD//////+AAAH//////wAAAP/////+AAAAf/////wAAAA/////+AAAAB/////wAAAAD////+ + AAAAAH////wAAAAAP///+AAAAAAf///wAAAAAA///+AAAAAAB///wAABgAAD//+AAAPAAAH//wAAB+AA + AP/+AAAP8AAAf/wAAB/4AAA/+AAAP/wAAB/wAAB//gAAD+AAAP//AAAH4AAB//+AAAf4AAP//8AAH/wA + B///4AAf/gAP///wAD//AB////gAf/+AP////AD//8B////+Af//4P////8D///x/////4f//+H///// + B///wP////4D//+Af////AH//wA////4AP/+AB////AAf/wAB///4AA/+AAD///AAB/4AAH//4AAH/wA + AP//AAA//gAAf/4AAH//AAA//AAA//+AAB/4AAH//8AAD/AAA///4AAH4AAH///wAAPAAA////gAAYAA + H////AAAAAA////+AAAAAH////8AAAAA/////4AAAAH/////wAGAA//////gA8AH//////AH4A////// + +A/wH//////8H/g///////4//H//////////////KAAAAIAAAAAAAQAAAQAgAAAAAAAAAAEAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAACK/gAAiv4TAIr+YACK/hsAiv4AAIz+AACM/gkAi/5NAIv+HACL/gAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAif4AAIn+EgCK/poAiv72AIv+XwCL + /gAAjP4AAIz+HwCL/tQAi/6xAIv+GwCL/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAIn+AACJ/hQAif6fAIn+9gCK/o8Aiv4QAIr+AACM/gAAkv4BAIv+VgCL/ukAi/60AIr+HwCK + /gAAiv4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACI/gAAiP4VAIn+pACJ/vQAif6GAIr+DQCM + /gEAi/4VAIz+DgCL/gAAi/4AAIv+VQCK/ukAiv65AIn+JACK/gAAif4AAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAiP4AAIj+FgCI/qcAiP7yAIn+fACJ/gsAiv4AAIv+PACL/ssAjP6oAIz+FwCM/gAAk/4AAIr+VQCK + /ukAif6+AIn+KgCK/gAAiP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIf+AACH/hYAh/6qAIj+8ACI/nMAif4IAIr+AACK + /jMAi/7NAIv+/wCM/v8AjP6qAIz+FwCM/gAAjv4AAIr+VACJ/ugAif7GAIj+MgCI/gAAiP4AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACG + /gAAhv4XAIf+rACH/u4AiP5rAIj+BgCK/gAAiv4qAIr+wwCL/v8Ai/7/AIz+/wCM/v8Ai/6qAIv+FwCL + /gAAif4AAIn+UQCI/uUAiP7OAIj+OgCQ/gAAh/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhv4AAIb+FwCG/qwAhv7tAIf+ZQCH/gQAif4AAIn+IwCK + /rwAiv7/AIv+/wCL/v8AjP7/AIz+/wCL/v8Ai/6qAIv+FwCL/gAAlP4AAIj+TQCI/uIAiP7WAIf+QgB7 + /gAAh/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIX9AACE + /RcAhf6sAIb+6wCG/l8Aif4CAIn+AACJ/h4Aif61AIn+/wCK/v8Ai/7/AIv+/wCM/v8AjP7/AIv+/wCL + /v8Aiv6qAIr+FwCK/gAAjP4AAIj+SACH/t4Ah/7dAIb+RwCB/gAAhv4AAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACE/QAAhP0XAIT9rACF/eoAhv5bAIf+AQCI/gAAiP4aAIn+sACJ + /v8Aif7/AIr+/wCL/v8Ai/7/AIz+/wCM/v8Ai/7/AIv+/wCK/v8Aiv6qAIr+FwCJ/gAAl/4AAIf+QwCG + /tcAhv7iAIX+TAB+/gAAhf4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAg/0AAIP9FwCE + /awAhP3pAIX9WACG/wEAh/4AAIj+GACI/q0AiP7/AIn+/wCK/v8Aiv7/AIv+/wCL/v8AjP7/AIz+/wCL + /v8Ai/7/AIr+/wCK/v8Aif6qAIn+FwCJ/gAAfP4AAIb+PACG/s8Ahf7lAIX9UAB7+AAAhP0AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAIP9AACD/RcAg/2sAIP96QCE/VYAjP0BAIf+AACH/hcAh/6rAIj+/wCI + /v8Aif7/AIr+/wCK/v8Ai/7/AIv+/wCM/v8AjP7/AIv+/wCL/v8Aiv7/AIr+/wCJ/v8Aif6qAIj+FwCI + /gAAg/4AAIb+NACF/scAhf3nAIT9UwCE/QAAhP0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACC/QAAgf0XAIL9rACD + /ekAg/1WAIz9AACG/gAAhv4XAIf+qgCH/v8AiP7/AIn+/wCJ/v8Aiv7/AIr+/wCL/v8Ai/7/AIz+/wCM + /v8Ai/7/AIv+/wCK/v8Aiv7/AIn+/wCI/v8AiP6qAIj+FwCI/gAAhf0AAIX+LACE/cAAhP3pAIP9VQB/ + /QAAg/0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAgf0AAIH9FwCB/awAgv3pAIP9VQCI/QAAhf4AAIb+FwCG/qoAhv7/AIf+/wCI + /v8Aif7/AIn+/wCK/v8Aiv7/AIv+/wCL/v8AjP7/AIz+/wCL/v8Ai/7/AIr+/wCK/v8Aif7/AIj+/wCI + /v8Ah/6qAIf+FwCH/gAAg/0AAIT9JQCE/bkAg/3pAIL9VQB9/QAAgv0AAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAID9AACA/RcAgf2sAIH96QCC + /VUAh/0AAIX+AACF/hcAhf6qAIb+/wCG/v8Ah/7/AIj+/wCJ/v8Aif7/AIr+/wCK/v8Ai/7/AIv+/wCM + /v8AjP7/AIv+/wCL/v8Aiv7/AIr+/wCJ/v8AiP7/AIj+/wCH/v8Ah/6qAIb+FwCG/gAAg/0AAIP9HwCD + /bQAgv3pAIH9VQB8/QAAgf0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAB//QAAf/0XAID9rACA/ekAgf1VAIH9AACE/QAAhP0XAIX9qgCF/v8Ahv7/AIf+/wCH + /v8AiP7/AIn+/wCJ/v8Aiv7/AIr+/wCL/v8Ai/7/AIz+/wCM/v8Ai/7/AIv+/wCK/v8Aiv7/AIn+/wCI + /v8AiP7/AIf+/wCG/v8Ahv6qAIb+FwCG/gAAgv0AAIP9HACC/bEAgf3pAIH9VQB3/QAAgP0AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfv0AAH79FwB//awAf/3pAID9VQCK + /QAAg/0AAIP9FwCE/aoAhf3/AIX+/wCG/v8Ah/7/AIf+/wCI/v8Aif7/AIn+/wCK/v8Aiv7/AIv+/wCL + /v8AjP7/AIz+/wCL/v8Ai/7/AIr+/wCK/v8Aif7/AIj+/wCI/v8Ah/7/AIb+/wCG/v8Ahf6qAIX+FwCF + /gAAgf0AAIL9GQCB/a4Agf3pAID9VQB2/QAAf/0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAH79AAB+/RcAfv2sAH796QB//VUAif0AAIL9AACD/RcAg/2qAIP9/wCF/f8Ahf7/AIb+/wCH + /v8Ah/7/AIj+/wCJ/v8Aif7/AIr+/wCK/v8Ai/7/AIv+/wCM/v8AjP7/AIv+/wCL/v8Aiv7/AIr+/wCJ + /v8AiP7/AIj+/wCH/v8Ahv7/AIb+/wCF/v8Ahf2qAIT9FwCE/QAAgf0AAIH9FwCA/awAgP3pAH/9VQB1 + /QAAfv0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB9/QAAfP0XAH39rAB+/ekAfv1VAIj9AACB + /QAAgf0XAIL9qgCD/f8Ag/3/AIX9/wCF/v8Ahv7/AIf+/wCH/v8AiP7/AIn+/wCJ/v8Aiv7/AIr+/wCL + /v8Ai/7/AIz+/wCM/v8Ai/7/AIv+/wCK/v8Aiv7/AIn+/wCI/v8AiP7/AIf+/wCG/v8Ahv7/AIX+/wCF + /f8AhP2qAIP9FwCD/QAAgP0AAID9FwB//awAf/3pAH79VQB+/QAAfv0AAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAfP0AAHv9FwB8/awAff3pAH79VQCD/QAAgf0AAIH9FwCB/aoAgv3/AIP9/wCD/f8Ahf3/AIX+/wCG + /v8Ah/7/AIf+/wCI/v8Aif7/AIn+/wCK/v8Aiv7/AIv+/wCL/v8AjP7/AIz+/wCL/v8Ai/7/AIr+/wCK + /v8Aif7/AIj+/wCI/v8Ah/7/AIb+/wCG/v8Ahf7/AIX9/wCD/f8Ag/2qAIP9FwCD/QAAf/0AAH/9FwB+ + /awAfv3pAH39VQB4/QAAff0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHv9AAB6/RcAe/2sAHz96QB9/VUAgv0AAID9AACA + /RcAgf2qAIH9/wCC/f8Ag/3/AIP9/wCF/f8Ahf7/AIb+/wCH/v8Ah/7/AIj+/wCJ/v8Aif7/AIr+/wCK + /v8Ai/7/AIv+/wCM/v8AjP7/AIv+/wCL/v8Aiv7/AIr+/wCJ/v8AiP7/AIj+/wCH/v8Ahv7/AIb+/wCF + /v8Ahf3/AIP9/wCD/f8Agv2qAIH9FwCC/QAAfv0AAH79FwB+/awAff3pAHz9VQB3/QAAfP0AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB6 + /QAAef0XAHr9rAB7/ekAfP1VAIH9AAB//QAAf/0XAID9qgCA/f8Agf3/AIL9/wCD/f8AhP3/AIX9/wCF + /v8Ahv7/AIf+/wCH/v8AiP7/AIn+/wCJ/v8Aiv7/AIr+/wCL/v8Ai/7/AIz+/wCM/v8Ai/7/AIv+/wCK + /v8Aiv7/AIn+/wCI/v8AiP7/AIf+/wCG/v8Ahv7/AIX+/wCF/f8Ag/3/AIP9/wCC/f8Agf2rAIH9GACB + /QAAff0AAH79FwB9/awAfP3pAHv9VQB2/QAAe/0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAef0AAHj9FwB5/awAev3pAHv9VQCA/QAAfv0AAH/9FwB/ + /aoAf/3/AID9/wCC/f8Agv3/AIP9/wCE/f8AhP3/AIX+/wCG/v8Ah/7/AIf+/wCI/v8Aif7/AIn+/wCK + /v8Aiv7/AIv+/wCL/v8AjP7/AIz+/wCL/v8Ai/7/AIr+/wCK/v8Aif7/AIj+/wCI/v8Ah/7/AIb+/wCG + /v8Ahf7/AIX9/wCD/f8Ag/3/AIL9/wCB/f8Agf2tAID9GgCA/QAAfP0AAH39FwB8/awAe/3pAHr9VQB1 + /QAAev0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHj9AAB3 + /RcAeP2sAHn96QB6/VUAf/0AAH39AAB9/RcAfv2qAH/9/wB//f8Agf3/AIL9/wCC/f8Ag/3/AIT9/wCF + /f8Ahf7/AIb+/wCH/v8Ah/7/AIj+/wCJ/v8Aif7/AIr+/wCK/v8Ai/7/AIv+/wCM/v8AjP7/AIv+/wCL + /v8Aiv7/AIr+/wCJ/v8Aif7/AIj+/wCH/v8Ahv7/AIb+/wCF/v8Ahf3/AIP9/wCD/f8Agv3/AIH9/wCB + /f8AgP2wAH/9HQB//QAAe/0AAHz9FwB7/awAev3pAHn9VQB0/QAAef0AAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB3/QAAd/0XAHf9rAB4/ekAef1VAH79AAB8/QAAfP0XAH39qgB+ + /f8Af/3/AH/9/wCB/f8Agv3/AIL9/wCD/f8AhP3/AIX9/wCF/v8Ahv7/AIf+/wCH/v8AiP7/AIn+/wCJ + /v8Aiv7/AIr+/wCL/v8Ai/7/AIz+/wCM/v8Ai/7/AIv+/wCK/v8Aiv7/AIn+/wCJ/v8AiP7/AIf+/wCG + /v8Ahv7/AIX9/wCE/f8Ag/3/AIP9/wCC/f8Agf3/AIH9/wB//f8Af/20AH79IQB+/QAAev0AAHv9FwB6 + /awAef3pAHj9VQBz/QAAeP0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdvwAAHb8FwB3 + /KwAd/3pAHj9VQB7/QAAe/0AAHv9FwB8/aoAff3/AH79/wB//f8Af/3/AIH9/wCC/f8Agv3/AIP9/wCE + /f8Ahf3/AIX+/wCG/v8Ahv7/AIf+/wCI/v8Aif7/AIn+/wCK/v8Aiv7/AIv+/wCL/v8AjP7/AIz+/wCL + /v8Ai/7/AIr+/wCJ/v8Aif7/AIn+/wCI/v8Ah/7/AIb+/wCG/v8Ahf3/AIT9/wCE/f8Ag/3/AIL9/wCB + /f8AgP3/AID9/wB+/f8Afv25AH39JwB9/QAAef0AAHr9FwB5/awAeP3pAHf9VQBy/QAAd/0AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAHX8AAB1/BcAdvysAHb86QB3/VUAgv8AAHr9AAB6/RcAe/2qAHz9/wB9 + /f8Afv3/AH/9/wB//f8Agf3/AIL9/wCC/f8Ag/3/AIT9/wCF/f8Ahf7/AIb+/wCH/v8Ah/7/AIj+/wCI + /v8Aif7/AIr+/wCK/v8Ai/7/AIv+/wCM/v8AjP7/AIv+/wCL/v8Aiv7/AIn+/wCJ/v8Aif7/AIj+/wCH + /v8Ahv7/AIb+/wCF/f8AhP3/AIP9/wCD/f8Agv3/AIH9/wCA/f8AgP3/AH79/wB9/f8Aff2/AHz9LQB9 + /QAAeP0AAHn9FwB4/awAd/3pAHb8VQBx9wAAdvwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB0/AAAc/wXAHX8rAB1 + /OkAdvxVAID8AAB6/QAAev0XAHr9qgB7/f8AfP3/AH39/wB+/f8Af/3/AH/9/wCB/f8Agv3/AIL9/wCD + /f8AhP3/AIX9/wCF/v8Ahv7/AIf+/wCI/v8AiP7/AIj+/wCJ/v8Aiv7/AIr+/wCL/v8Ai/7/AIz+/wCM + /v8Ai/7/AIv+/wCK/v8Aif7/AIn+/wCJ/v8AiP7/AIf+/wCG/v8Ahv7/AIX9/wCE/f8Ag/3/AIP9/wCC + /f8Agf3/AID9/wCA/f8Afv3/AH39/wB9/f8AfP3GAHv9NAB+/QAAeP0AAHj9FwB3/awAdvzpAHX8VQBw + /AAAdfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAc/wAAHL8FwBz/KwAdPzpAHX8VQB//AAAef0AAHn9FwB6/aoAev3/AHv9/wB8 + /f8Aff3/AH79/wB//f8AgP3/AID9/wCC/f8Agv3/AIP9/wCE/f8Ahf3/AIX+/wCG/v8Ah/7/AIj+/wCI + /v8AiP7/AIn+/wCK/v8Aiv7/AIv+/wCL/v8AjP7/AIz+/wCL/v8Ai/7/AIr+/wCJ/v8Aif7/AIn+/wCI + /v8Ah/7/AIb+/wCG/v8Ahf3/AIT9/wCE/f8Ag/3/AIL9/wCB/f8AgP3/AID9/wB+/f8Afv3/AHz9/wB8 + /f8Ae/3NAHr9PAB//QAAd/wAAHf8FwB2/KwAdfzpAHT8VQBv/AAAdPwAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHL8AABx/BcAcvysAHP86QB0 + /FUAfvwAAHj9AAB4/RcAef2qAHn9/wB6/f8Ae/3/AHz9/wB9/f8Afv3/AH/9/wCA/f8AgP3/AIL9/wCC + /f8Ag/3/AIT9/wCF/f8Ahf7/AIb+/wCH/v8AiP7/AIj+/wCI/v8Aif7/AIr+/wCL/v8Ai/7/AIz+/wCM + /v8AjP7/AIv+/wCL/v8Aiv7/AIn+/wCJ/v8Aif7/AIj+/wCH/v8Ahv7/AIb+/wCF/f8AhP3/AIT9/wCD + /f8Agv3/AIH9/wCA/f8AgP3/AH79/wB9/f8AfP3/AHz9/wB7/f8Aev3UAHn9QgCh/QAAdvwAAHb8FwB1 + /KwAdPzpAHP8VQBu/AAAc/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAABx/AAAcPwXAHH8rABy/OkAc/xVAIL8AAB3/QAAd/0XAHj9qgB4/f8Aef3/AHv9/wB7 + /f8AfP3/AH39/wB+/f8Af/3/AID9/wCA/f8Agv3/AIL9/wCD/f8AhP3/AIX9/wCF/v8Ahv7/AIf+/wCI + /v8AiP7/AIn+/wCJ/v8Aiv7/AIv+/wCL/v8AjP7/AIz+/wCM/v8Ai/7/AIv+/wCK/v8Aif7/AIn+/wCJ + /v8AiP7/AIf+/wCG/v8Ahv7/AIX9/wCE/f8AhP3/AIL9/wCC/f8Agf3/AID9/wCA/f8Afv3/AH39/wB8 + /f8AfP3/AHv9/wB6/f8Aef3aAHj9SABp/QAAdfwAAHX8FwB0/KwAc/zpAHL8VQBk/AAAcfwAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcPwAAG/8FwBw/KwAcfzpAHL8VQB3 + /AAAdfwAAHb8FwB3/aoAd/3/AHj9/wB6/f8Ae/3/AHv9/wB8/f8Aff3/AH79/wB//f8AgP3/AID9/wCC + /f8Agv3/AIP9/wCE/f8AhP3/AIX+/wCG/v8Ah/7/AIf+/wCI/v8Aif7/AIn+/wCK/v8Ai/7/AIv+/wCM + /v8AjP7/AIz+/wCL/v8Ai/7/AIr+/wCJ/v8Aif7/AIn+/wCI/v8Ah/7/AIb+/wCG/v8Ahf3/AIT9/wCE + /f8Agv3/AIL9/wCB/f8AgP3/AID9/wB+/f8Aff3/AHz9/wB8/f8Ae/3/AHr9/wB5/f8AeP3fAHf9TABw + /QAAdPwAAHT8FwBz/KwAcvzpAHH8VQBn/AAAcPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAG/8AABu/BcAb/ysAHD86QBx/FUAdvwAAHT8AAB1/BcAdvyqAHb9/wB3/f8Aef3/AHr9/wB7 + /f8Ae/3/AHz9/wB9/f8Afv3/AH/9/wCA/f8AgP3/AIL9/wCC/f8Ag/3/AIT9/wCE/f8Ahf7/AIb+/wCH + /v8AiP7/AIj+/wCJ/v8Aif7/AIr+/wCL/v8Ai/7/AIz+/wCM/v8AjP7/AIv+/wCL/v8Aiv7/AIn+/wCJ + /v8Aif7/AIj+/wCH/v8Ah/7/AIb+/wCF/f8AhP3/AIT9/wCC/f8Agv3/AIH9/wCA/f8AgP3/AH79/wB9 + /f8Aff3/AHv9/wB7/f8Aev3/AHn9/wB4/f8Ad/3jAHb8UABx9wAAc/wAAHP8FwBx/KwAcfzpAHD8VQBm + /AAAb/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABu/AAAbfwXAG78rABv/OkAcPxVAHX8AABz + /AAAdPwXAHX8qgB1/P8Advz/AHj9/wB5/f8Aev3/AHv9/wB7/f8AfP3/AH39/wB+/f8Af/3/AID9/wCA + /f8Agv3/AIL9/wCD/f8AhP3/AIT9/wCG/v8Ahv7/AIf+/wCI/v8AiP7/AIn+/wCJ/v8Aiv7/AIv+/wCL + /v8AjP7/AIz+/wCM/v8Ai/7/AIv+/wCK/v8Aif7/AIn+/wCI/v8AiP7/AIf+/wCH/v8Ahf7/AIX9/wCE + /f8AhP3/AIL9/wCC/f8Agf3/AID9/wCA/f8Afv3/AH39/wB8/f8Ae/3/AHv9/wB6/f8Aef3/AHj9/wB3 + /f8AdvzmAHX8UgBx/AAAcvwAAHH8FwBw/KwAcPzpAG/8VQBl/AAAbvwAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAbfwAAGz8FwBt/KwAbvzpAG/8VQB0/AAAcvwAAHP8FwB0/KoAdPz/AHX8/wB3/f8AeP3/AHn9/wB6 + /f8Aev3/AHz9/wB8/f8Aff3/AH79/wB//f8AgP3/AID9/wCC/f8Agv3/AIP9/wCE/f8AhP3/AIb+/wCG + /v8Ah/7/AIj+/wCI/v8Aif7/AIn+/wCK/v8Ai/7/AIv+/wCL/vEAjP7xAIz+/wCL/v8Ai/7/AIr+/wCJ + /v8Aif7/AIj+/wCI/v8Ah/7/AIf+/wCF/v8Ahf3/AIT9/wCE/f8Agv3/AIL9/wCB/f8AgP3/AID9/wB+ + /f8Aff3/AHz9/wB7/f8Ae/3/AHr9/wB5/f8AeP3/AHf9/wB2/P8AdfznAHT8VABn/AAAcPwAAHD8FwBv + /KwAb/zpAG78VQBk/AAAbfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGz8AABr/BcAbPysAG386QBu/FUAc/wAAHH8AABy + /BcAc/yqAHP8/wB0/P8Advz/AHf9/wB4/f8Aef3/AHr9/wB7/f8AfP3/AHz9/wB9/f8Afv3/AH/9/wCA + /f8AgP3/AIL9/wCC/f8Ag/3/AIT9/wCE/f8Ahv7/AIb+/wCH/v8AiP7/AIj+/wCJ/v8Aif7/AIr+/wCL + /v8Ai/7oAIv+VQCM/lUAjP7oAIv+/wCL/v8Aiv7/AIr+/wCJ/v8AiP7/AIj+/wCH/v8Ah/7/AIX+/wCF + /f8AhP3/AIT9/wCC/f8Agv3/AIH9/wCA/f8Af/3/AH/9/wB9/f8AfP3/AHz9/wB6/f8Aev3/AHn9/wB4 + /f8Ad/3/AHb8/wB1/P8AdPzoAHP8VQBq/AAAb/wAAG/8FwBu/KwAbvzpAG38VQBj/AAAbPwAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABr + /AAAavwXAGv8rABs/OkAbfxVAHL8AABw/AAAcPwXAHL8qgBy/P8Ac/z/AHX8/wB2/P8Ad/3/AHj9/wB5 + /f8Aev3/AHv9/wB8/f8AfP3/AH39/wB+/f8Af/3/AID9/wCA/f8Agf3/AIP9/wCD/f8AhP3/AIT9/wCG + /v8Ahv7/AIf+/wCI/v8AiP7/AIn+/wCJ/v8Aiv7/AIr+6ACL/lUAiv4AAJ3+AACL/lUAi/7oAIr+/wCK + /v8Aiv7/AIn+/wCI/v8AiP7/AIf+/wCH/v8Ahf7/AIX9/wCE/f8AhP3/AIL9/wCC/f8Agf3/AID9/wB/ + /f8Af/3/AH39/wB8/f8Ae/3/AHr9/wB6/f8Aef3/AHj9/wB3/f8Advz/AHX8/wB0/P8Ac/zoAHL8VQBp + /AAAbvwAAG78FwBt/KwAbPzpAGv8VQBm/AAAa/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAafwAAGn8FwBq/KwAa/zpAGz8VQBx/AAAb/wAAG/8FwBw + /KoAcfz/AHP8/wB0/P8Adfz/AHb8/wB3/f8AeP3/AHn9/wB6/f8Ae/3/AHz9/wB8/f8Aff3/AH79/wB/ + /f8AgP3/AID9/wCC/f8Ag/3/AIP9/wCE/f8AhP3/AIb+/wCG/v8Ah/7/AIj+/wCI/v8Aif7/AIn+/wCK + /ugAiv5VAJP+AACL/gAAi/4AAIv+AACL/lUAiv7oAIr+/wCK/v8Aif7/AIj+/wCI/v8Ah/7/AIb+/wCF + /v8Ahf3/AIT9/wCE/f8Agv3/AIL9/wCB/f8AgP3/AH/9/wB//f8Aff3/AHz9/wB7/f8Ae/3/AHn9/wB5 + /f8AeP3/AHf8/wB2/P8Adfz/AHT8/wBy/P8AcvzoAHH8VQBo/AAAbfwAAG38FwBs/KwAa/zpAGr8VQBl + /AAAavwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGj8AABo + /BcAafysAGr86QBr/FUAcPwAAG78AABu/BcAb/yqAHD8/wBy/P8Acvz/AHT8/wB1/P8Advz/AHf9/wB4 + /f8Aef3/AHr9/wB7/f8Ae/3/AH39/wB9/f8Afv3/AH/9/wCA/f8Agf3/AIH9/wCD/f8Ag/3/AIT9/wCE + /f8Ahv7/AIb+/wCH/v8AiP7/AIj+/wCJ/v8Aif7oAIr+VQCO/gAAiv4AAAAAAAAAAAAAi/4AAJP+AACK + /lUAiv7oAIr+/wCJ/v8AiP7/AIj+/wCH/v8Ahv7/AIX+/wCF/f8AhP3/AIT9/wCC/f8Agv3/AIH9/wCA + /f8Af/3/AH/9/wB9/f8AfP3/AHv9/wB6/f8Aef3/AHn9/wB4/f8Ad/z/AHb8/wB1/P8Ac/z/AHP8/wBx + /P8AcfzoAHD8VQBn/AAAbPwAAGz8FwBr/KwAavzpAGn8VQBk/AAAafwAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + 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 + /P8Abfz/AGz8/wBr/P8Aavz/AGn8/wBo+/8AZ/voAGb7VQBd+wAAYvsAAGL7FwBh+6wAYPvpAF/7VQBR + +wAAXvsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABd+wAAXfsXAF77rABf++kAYPtVAGX7AABj + +wAAZPsXAGX7qgBl+/8AZvv/AGj7/wBp/P8Aavz/AGv8/wBs/P8Abfz/AG78/wBw/P8Acfz/AHL8/wBz + /P8AdPz/AHX8/wB2/P8Ad/3/AHj9/wB5/f8Aev3/AHv9/wB8/f8Aff3/AH79/wB+/f8Af/3/AID9/wCB + /f8Agf3/AIL96ACD/VUAg/0AAIP9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhP0AAIj9AACE + /VUAg/3oAIP9/wCB/f8Agf3/AID9/wB//f8Afv3/AH79/wB8/f8Ae/3/AHr9/wB5/f8AeP3/AHf9/wB2 + /P8Adfz/AHT8/wBz/P8Acvz/AHH8/wBx/P8Ab/z/AG78/wBt/P8AbPz/AGv8/wBq/P8Aafz/AGj7/wBm + +/8AZvvoAGX7VQBc+wAAYfsAAGH7FwBg+6wAX/vpAF77VQBU+wAAXfsAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAXPsAAFz7FwBd+6wAXvvpAF/7VQBk+wAAYvsAAGL7FwBk+6oAZPv/AGX7/wBm+/8AaPv/AGn8/wBq + /P8Aa/z/AGz8/wBt/P8Ab/z/AG/8/wBx/P8Acvz/AHP8/wB0/P8Adfz/AHb8/wB3/f8AeP3/AHn9/wB6 + /f8Ae/3/AHz9/wB9/f8Afv3/AH79/wB//f8AgP3/AIH9/wCB/egAgv1VAIv9AACD/QAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAg/0AAIP9AACD/VUAgv3oAIH9/wCB/f8AgP3/AH/9/wB+ + /f8Afv3/AHz9/wB7/f8Aev3/AHn9/wB4/f8Ad/3/AHb8/wB1/P8AdPz/AHP8/wBy/P8Acfz/AHH8/wBv + /P8Abvz/AG38/wBs/P8Aa/z/AGr8/wBp/P8AaPv/AGb7/wBl+/8AZPvoAGP7VQBf+wAAYPsAAGD7FwBe + +6wAXvvpAF37VQBT+wAAXPsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFv7AABb+xcAXPusAFz76QBd+1UAZ/sAAGH7AABh + +xcAYvuqAGP7/wBl+/8AZfv/AGf7/wBo+/8Aafz/AGr8/wBr/P8AbPz/AG38/wBv/P8AcPz/AHH8/wBy + /P8Ac/z/AHT8/wB1/P8Advz/AHf9/wB4/f8Aef3/AHr9/wB7/f8AfP3/AH39/wB+/f8Afv3/AH/9/wCA + /f8Agf3pAIH9VQCK/QAAgv0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAg/0AAIv9AACC/VUAgf3oAIH9/wCA/f8Af/3/AH79/wB9/f8Aff3/AHv9/wB6/f8Aef3/AHj9/wB3 + /f8Advz/AHX8/wB0/P8Ac/z/AHL8/wBx/P8Acfz/AG/8/wBu/P8Abfz/AGz8/wBr/P8Aavz/AGn8/wBn + +/8AZ/v/AGb7/wBk+/8AY/voAGL7VQBe+wAAX/sAAF77FwBd+6wAXfvpAFz7VQBS+wAAW/sAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABa + +wAAWvsXAFv7rABb++kAXPtVAGb7AABg+wAAYPsXAGH7qgBi+/8AY/v/AGX7/wBl+/8AZ/v/AGj7/wBp + /P8Aavz/AGv8/wBs/P8Abfz/AG/8/wBw/P8Acfz/AHL8/wBz/P8AdPz/AHX8/wB2/P8Ad/3/AHj9/wB5 + /f8Aev3/AHv9/wB8/f8Aff3/AH79/wB+/f8Af/3/AID96QCB/VYAhP0AAIH9AAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgv0AAIr9AACB/VUAgf3oAID9/wB/ + /f8Afv3/AH39/wB9/f8Ae/3/AHr9/wB5/f8AeP3/AHf9/wB2/P8Adfz/AHT8/wBz/P8Acvz/AHH8/wBx + /P8Ab/z/AG78/wBt/P8AbPz/AGv8/wBq/P8Aafz/AGf7/wBm+/8AZvv/AGT7/wBj+/8AYvvoAGH7VQBd + +wAAXvsAAF37FwBc+6wAXPvpAFv7VQBS+wAAWvsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWfoAAFn6FwBa+qwAWvvpAFv7VQBl+wAAX/sAAF/7FgBg + +6oAYfv/AGL7/wBj+/8AZfv/AGX7/wBn+/8AaPv/AGn8/wBq/P8Aa/z/AGz8/wBt/P8Ab/z/AHD8/wBx + /P8Acvz/AHP8/wB0/P8Adfz/AHb8/wB3/f8AeP3/AHn9/wB6/f8Ae/3/AHz9/wB9/f8Aff3/AH/9/wB/ + /eoAgP1ZAIL9AQCA/QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAgf0AAIX9AACB/VUAgP3oAH/9/wB+/f8Aff3/AH39/wB7/f8Aev3/AHn9/wB4 + /f8Ad/3/AHb8/wB1/P8AdPz/AHP8/wBy/P8Acvz/AHD8/wBv/P8Abvz/AG38/wBs/P8Aa/z/AGr8/wBp + /P8AZ/v/AGb7/wBm+/8AZPv/AGP7/wBi+/8AYfvoAGD7VQBd+wAAXfsAAFz7FwBb+6wAWvvpAFn6VQBU + 9QAAWfoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFj6AABX + +ggAWfqfAFn66gBa+1UAZP8AAF77AABe+xYAX/uoAGD7/wBh+/8AYvv/AGT7/wBk+/8AZfv/AGf7/wBo + +/8Aafz/AGr8/wBr/P8AbPz/AG38/wBv/P8AcPz/AHH8/wBy/P8Ac/z/AHT8/wB1/P8Adfz/AHb9/wB3 + /f8Aef3/AHr9/wB7/f8AfP3/AH39/wB+/f8Afv3sAH/9XgB//QEAf/0AAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgP0AAIT9AACA + /VUAf/3oAH79/wB9/f8Aff3/AHv9/wB6/f8Aef3/AHj9/wB3/f8Advz/AHX8/wB0/P8Ac/z/AHL8/wBy + /P8AcPz/AG/8/wBu/P8Abfz/AGz8/wBr/P8Aavz/AGn8/wBn+/8AZvv/AGb7/wBk+/8AY/v/AGL7/wBh + +/8AYPvoAF/7VQBU+wAAW/sAAFv7FwBa+6wAWfrnAFj6QABZ+gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWPoAAFf6AgBZ+k8AWfpNAFz6AQBd+wAAXfsUAF77pQBf + +/4AYPv/AGH7/wBi+/8AZPv/AGX7/wBl+/8AZ/v/AGj7/wBp/P8Aavz/AGv8/wBs/P8Abvz/AG78/wBw + /P8Acfz/AHL8/wBz/P8AdPz/AHT8/wB2/P8Ad/3/AHj9/wB4/f8Aev3/AHv9/wB8/f8Aff3/AH397gB+ + /WUAgf0CAH/9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/0AAIP9AAB//VUAfv3oAH39/wB9/f8Ae/3/AHr9/wB5 + /f8AeP3/AHf9/wB2/P8Adfz/AHT8/wBz/P8Acvz/AHL8/wBw/P8Ab/z/AG78/wBt/P8AbPz/AGv8/wBq + /P8Aafz/AGf7/wBn+/8AZfv/AGT7/wBj+/8AYvv/AGH7/wBg+/8AX/voAF77VQBV+wAAWvoAAFn6GQBZ + +mwAWfocAFn6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAXPsAAFz7EwBd+6AAXvv9AF/7/wBg+/8AYfv/AGL7/wBk+/8AZfv/AGX7/wBn + +/8AaPv/AGn8/wBq/P8Aa/z/AGz8/wBu/P8Ab/z/AHD8/wBx/P8Acvz/AHP8/wBz/P8Adfz/AHb8/wB3 + /f8AeP3/AHn9/wB5/f8Ae/3/AHz9/wB8/fEAff1uAH/9BAB+/QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAfv0AAIL9AAB+/VUAff3oAH39/wB7/f8Aev3/AHn9/wB4/f8Ad/3/AHb8/wB1/P8AdPz/AHP8/wBy + /P8Acvz/AHD8/wBv/P8Abvz/AG38/wBs/P8Aa/z/AGr8/wBp/P8AZ/v/AGf7/wBl+/8AZPv/AGP7/wBi + +/8AYfv/AGD7/wBe+/8AXvvoAF37VgBX+wEAXPsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABc+wAAXPtiAF37+gBe + +/8AX/v/AGD7/wBh+/8AYvv/AGT7/wBk+/8AZvv/AGf7/wBo+/8Aafz/AGr8/wBr/P8AbPz/AG78/wBv + /P8AcPz/AHH8/wBy/P8Acvz/AHT8/wB1/P8Advz/AHf8/wB4/f8Aef3/AHn9/wB7/f8Ae/31AHz9eQB+ + /QcAff0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAff0AAH/9AAB9/VUAfP3oAHz9/wB6 + /f8Aef3/AHj9/wB3/f8Advz/AHX8/wB0/P8Ac/z/AHP8/wBx/P8AcPz/AG/8/wBu/P8Abfz/AGz8/wBr + /P8Aavz/AGj8/wBo+/8AZ/v/AGX7/wBk+/8AY/v/AGL7/wBh+/8AYPv/AF77/wBd+/8AXfvNAFz7FQBc + +wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAFz7AABc+xgAXfuqAF77/wBf+/8AYPv/AGH7/wBi+/8AZPv/AGT7/wBm + +/8AZ/v/AGj7/wBp/P8Aavz/AGv8/wBs/P8Abvz/AG/8/wBw/P8Acfz/AHL8/wBy/P8AdPz/AHX8/wB2 + /P8Ad/3/AHj9/wB5/f8Aev3/AHr9+AB7/YYAfP0KAHz9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAfP0AAHr9AAB8/VUAe/3oAHv9/wB5/f8AeP3/AHf9/wB2/P8Adfz/AHT8/wBz + /P8Ac/z/AHH8/wBw/P8Ab/z/AG78/wBt/P8AbPz/AGv8/wBq/P8AaPz/AGf7/wBn+/8AZfv/AGT7/wBj + +/8AYvv/AGH7/wBg+/8AXvv/AF776ABd+1YAV/sBAFz7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF77AABd + +xcAXvuqAF/7/wBg+/8AYfv/AGL7/wBk+/8AZPv/AGb7/wBn+/8AaPv/AGn8/wBq/P8Aa/z/AGz8/wBu + /P8Ab/z/AHD8/wBx/P8Acfz/AHP8/wB0/P8Adfz/AHb8/wB3/f8AeP3/AHn9/wB6/fsAev2RAHv9DgB7 + /QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfP0AAIT9AAB7 + /VMAev3nAHr9/wB4/f8Ad/3/AHb8/wB1/P8AdPz/AHP8/wBz/P8Acfz/AHD8/wBv/P8Abvz/AG38/wBs + /P8Aa/z/AGr8/wBo/P8AZ/v/AGf7/wBl+/8AZPv/AGP7/wBi+/8AYfv/AGD7/wBf++gAXvtVAFX7AABd + +wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF/7AABe+xcAX/uqAGD7/wBh+/8AY/v/AGP7/wBk + +/8AZvv/AGf7/wBo+/8Aafz/AGr8/wBr/P8Abfz/AG38/wBv/P8AcPz/AHH8/wBx/P8Ac/z/AHT8/wB1 + /P8Advz/AHf9/wB4/f8Aef39AHr9mgB6/REAev0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAe/0AAIT9AAB6/VEAef3lAHn9/wB3/f8Advz/AHX8/wB0 + /P8AdPz/AHL8/wBx/P8AcPz/AG/8/wBu/P8Abfz/AGz8/wBr/P8Aavz/AGj8/wBn+/8AZ/v/AGX7/wBk + +/8AY/v/AGL7/wBh+/8AYPvoAF/7VQBW+wAAXvsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAGD7AABf+xcAYPuqAGH7/wBj+/8AZPv/AGT7/wBm+/8AZ/v/AGj7/wBp/P8Aavz/AGv8/wBt + /P8Abvz/AG/8/wBw/P8AcPz/AHL8/wBz/P8AdPz/AHX8/wB2/P8Ad/3/AHj9/gB5/aEAev0TAHn9AAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAev0AAIX9AAB5/U4AeP3iAHj9/wB3/f8Adfz/AHT8/wB0/P8Acvz/AHH8/wBw/P8Ab/z/AG78/wBt + /P8AbPz/AGv8/wBq/P8AaPz/AGf7/wBn+/8AZfv/AGT7/wBj+/8AYvv/AGH76ABg+1UAV/sAAF/7AAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGH7AABg+xcAYvuqAGL7/wBj + +/8AZfv/AGb7/wBn+/8AaPz/AGn8/wBq/P8Aa/z/AG38/wBu/P8Ab/z/AHD8/wBw/P8Acvz/AHP8/wB0 + /P8Adfz/AHb8/wB3/f4AeP2mAHn9FQB4/QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAef0AAIr9AAB4/UoAd/3dAHf9/wB2 + /P8AdPz/AHT8/wBy/P8Acfz/AHD8/wBv/P8Abvz/AG38/wBs/P8Aa/z/AGn8/wBp/P8AaPv/AGb7/wBl + +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 + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAdfwAAHD8AAB0/DEAc/zDAHL8/wBx/P8AcPz/AG/8/wBu/P8Abfz/AGz8/wBr/P8Aafz/AGj7/wBn + +/8AZvvoAGX7VQBh+wAAZfsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGf7AABm+xcAZ/uqAGj8/wBp + /P8Aavz/AGz8/wBt/P8Abvz/AG78/wBw/P8Acfz/AHL8/wBz/KoAdPwXAHP8AAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAc/wAAHL8AABz/CoAcvy8AHH8/wBw + /P8Ab/z/AG78/wBt/P8AbPz/AGr8/wBq/P8AaPv/AGj76ABn+1UAXvsAAGb7AAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAGj7AABn+xcAaPyqAGn8/wBq/P8AbPz/AG38/wBu/P8Abvz/AHD8/wBx + /P8AcvyqAHP8FwBy/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAcvwAAHH8AABy/CQAcfy3AHD8/wBv/P8Abvz/AG38/wBs/P8Aavz/AGn8/wBp + /OgAaPtVAF/7AABn+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGn8AABo + /BcAafyqAGr8/wBs/P8Abfz/AG78/wBu/P8AcPz/AHH8qgBy/BcAcfwAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcfwAAHD8AABx + /B8AcPyyAG/8/wBu/P8Abfz/AGz8/wBq/P8AavzoAGn8VQBg8wAAaPsAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGr8AABq/BcAa/yqAGv8/wBs/P8Abvz/AG/8/wBv + /KoAcfwXAHD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG/8AABw/BsAb/yvAG78/wBt/P8AbPz/AGv86ABq + /FUAYfwAAGn8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAGv8AABr/BYAbPynAGz8/gBu/P8AbvyqAG/8FwBv/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAG78AABv/BgAbvy1AG38/wBs/OgAa/xVAGL8AABq/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa/wAAHD8AABs/GEAbfz6AG38tABu + /BUAbvwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbvwAAG/8FQBu/LMAbfz/AGz8lQBp + /AIAa/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAGr8AABi/AAAa/xVAGz86ABt/P8AbfyzAG78FQBu/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAG/8AABw/BcAb/yqAG78/wBt/P8AbPzoAGv8VQBi/AAAavwAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABp/AAAYfwAAGr8VQBr/OgAa/z/AG38/wBu + /P8AbvyqAHD8FwBv/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABw/AAAcfwXAHD8qgBv/P8Abvz/AG38/wBr + /P8Aa/zoAGr8VQBh/AAAafwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAaPsAAGDzAABp/FUAavzoAGr8/wBs/P8AbPz/AG78/wBv/P8AcPyqAHH8FwBw/AAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAcfwAAHL8FwBx/KoAcPz/AG/8/wBu/P8Abfz/AGv8/wBq/P8AavzoAGn8VQBh9AAAaPsAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGf7AABj+wAAZ/tVAGj86ABp/P8Aa/z/AGz8/wBs + /P8Abvz/AG/8/wBw/P8AcfyqAHL8FwBx/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHL8AABz/BcAcvyqAHH8/wBw/P8Ab/z/AG78/wBt + /P8Aa/z/AGr8/wBp/P8AaPzpAGf7WQBm+wEAZ/sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABm + +wAAYvsAAGb7VQBn++gAaPv/AGr8/wBr/P8AbPz/AGz8/wBu/P8Ab/z/AHD8/wBx/P8AcvyqAHP8FwBy + /AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABz + /AAAdPwXAHP8qgBy/P8Acfz/AHD8/wBv/P8Abvz/AGz8/wBs/P8Aavz/AGr8/wBo+/8AZ/vrAGb7YwBl + +wUAZvsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZfsAAGH7AABl+1UAZvvoAGf7/wBo/P8Aavz/AGv8/wBr + /P8Abfz/AG78/wBv/P8AcPz/AHH8/wBy/P8Ac/yqAHT8FwBz/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + 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 + /P8Aa/z/AGv8/wBt/P8Abvz/AG/8/wBw/P8Acfz/AHL8/wBz/P8AdPz/AHX8/wB2/P8Ad/2tAHj9HAB3 + /QAAeP0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAHj9AAB5/RcAeP2qAHf9/wB2/P8Adfz/AHT8/wBz/P8Acvz/AHH8/wBw + /P8Ab/z/AG38/wBs/P8Aa/z/AGv8/wBp/P8AaPv/AGf7/wBm+/8AZfv/AGT7/wBi+/8AYfv/AGH7qgBg + +xcAYPsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABf+wAAVvsAAGD7VQBh + ++gAYvv/AGP7/wBk+/8AZfv/AGb7/wBn+/8Aafz/AGr8/wBq/P8AbPz/AG38/wBu/P8Ab/z/AHD8/wBx + /P8Acvz/AHP8/wB0/P8Adfz/AHb8/wB3/f8AeP21AHn9JgB4/QAAef0AAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB5/QAAev0XAHn9qgB4 + /f8Ad/3/AHb8/wB1/P8AdPz/AHP8/wBy/P8Acfz/AHD8/wBv/P8Abfz/AGz8/wBr/P8Aa/z/AGn8/wBo + +/8AZ/v/AGb7/wBl+/8AZPv/AGL7/wBh+/8AYPv/AGD7qgBe+xcAX/sAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAXvsAADf7AABf+1MAYPvoAGD7/wBi+/8AY/v/AGT7/wBl+/8AZvv/AGf7/wBp + /P8Aavz/AGr8/wBs/P8Abfz/AG78/wBv/P8AcPz/AHH8/wBy/P8Ac/z/AHT8/wB1/P8Advz/AHf9/wB4 + /f8Aef3DAHr9NgBu/QAAev0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAev0AAHv9FwB6/aoAef3/AHj9/wB3/f8Advz/AHX8/wB0/P8Ac/z/AHL8/wBx + /P8AcPz/AG78/wBu/P8AbPz/AGv8/wBr/P8Aafz/AGj7/wBn+/8AZvv/AGX7/wBk+/8AYvv/AGL7/wBg + +/8AX/v/AF77qgBd+xcAXvsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF37AABk+wAAXvtLAF/74wBf + +/8AYPv/AGL7/wBj+/8AZPv/AGX7/wBm+/8AZ/v/AGn8/wBq/P8Aavz/AGz8/wBt/P8Abvz/AG/8/wBw + /P8Acfz/AHL8/wBz/P8AdPz/AHX8/wB2/P8Ad/3/AHj9/wB5/f8Aev3TAHv9RQCA/QAAe/0AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHv9AAB8/RYAe/2qAHr9/wB5 + /f8AeP3/AHf9/wB2/P8Adfz/AHT8/wBz/P8Acvz/AHH8/wBw/P8Abvz/AG38/wBs/P8AbPz/AGr8/wBp + /P8AaPv/AGf7/wBm+/8AZfv/AGT7/wBi+/8AYvv/AGD7/wBf+/8AXvv/AF37qwBc+xkAXfsAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAXfsAAFz7LgBe+9kAXvv/AGD7/wBg+/8AYvv/AGP7/wBk+/8AZfv/AGb7/wBn + +/8Aafz/AGr8/wBq/P8AbPz/AG38/wBu/P8Ab/z/AHD8/wBx/P8Acvz/AHP8/wB0/P8Adfz/AHb8/wB4 + /f8AeP3/AHn9/wB6/f8Ae/3gAHz9TwB//QAAfP0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAB8/QAAfP0UAHz9pQB7/f4Aev3/AHn9/wB4/f8Ad/3/AHb8/wB1/P8AdPz/AHP8/wBy + /P8Acfz/AHD8/wBu/P8Abfz/AGz8/wBs/P8Aavz/AGn8/wBo+/8AZ/v/AGb7/wBl+/8AY/v/AGP7/wBi + +/8AYPv/AF/7/wBe+/8AXfvnAFz7QABd+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABd+wAAXPsUAF77qgBe + +/8AX/v/AGD7/wBi+/8AY/v/AGT7/wBl+/8AZvv/AGf7/wBp/P8Aavz/AGr8/wBs/P8Abfz/AG78/wBv + /P8AcPz/AHH8/wBy/P8Ac/z/AHT8/wB1/P8Ad/z/AHf9/wB5/f8Aef3/AHr9/wB7/f8AfP3nAH39VACB + /QAAff0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAff0AAH39DwB8/ZkAfP39AHv9/wB6 + /f8Aef3/AHj9/wB3/f8Advz/AHX8/wB0/P8Ac/z/AHL8/wBx/P8AcPz/AG78/wBt/P8AbPz/AGz8/wBq + /P8Aafz/AGj7/wBn+/8AZvv/AGX7/wBj+/8AYvv/AGL7/wBg+/8AX/v/AF776wBd+1gAWvsAAF37AAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABe+wAAXvsXAF/7qgBf+/8AYfv/AGL7/wBj+/8AZPv/AGX7/wBm + +/8AaPv/AGj8/wBp/P8Aa/z/AGz8/wBt/P8Abvz/AG/8/wBw/P8Acfz/AHL8/wBz/P8AdPz/AHb8/wB2 + /P8AeP3/AHn9/wB5/f8Aev3/AHv9/wB8/f8Aff3oAH79VQCC/QAAfv0AAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAH79AAB+/QgAff2FAHz9+QB8/f8Ae/3/AHr9/wB5/f8AeP3/AHf9/wB2/P8Adfz/AHT8/wBz + /P8Acvz/AHH8/wBv/P8Ab/z/AG38/wBs/P8AbPz/AGr8/wBp/P8AaPv/AGf7/wBm+/8AZfv/AGP7/wBj + +/8AYfv/AGD7/wBf++8AXvtiAF37AgBe+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABf + +wAAX/sXAGD7qgBh+/8AYvv/AGP7/wBk+/8AZfv/AGb7/wBo+/8Aafv/AGn8/wBr/P8AbPz/AG38/wBu + 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 + /f8AfP3/AH39/wB+/f8Afv3/AH/9/wCA/f8Agf3/AIL9/wCD/f8Ag/3/AIT9/wCF/v8Ahv7/AIb+/wCH + /v8AiP7/AIj+/wCJ/v8Aiv7/AIr+/wCL/v8Ai/7/AIv+qgCM/hUAi/4VAIv+qgCK/v8Aiv7/AIr+/wCJ + /v8AiP7/AIf+/wCH/v8Ahv7/AIX+/wCF/f8AhP3/AIP9/wCD/f8Agf3/AIH9/wCA/f8Af/3/AH79/wB9 + /f8Aff3/AHv9/wB6/f8Aef3/AHj9/wB3/f8Adv3/AHb8qgB1/BcAdfwAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAB3/QAAd/0OAHj9fgB5/fEAef3/AHv9/wB8/f8Aff3/AH79/wB+/f8Af/3/AID9/wCB + /f8Agv3/AIL9/wCD/f8AhP3/AIX+/wCG/v8Ahv7/AIf+/wCI/v8AiP7/AIn+/wCK/v8Aiv7/AIr+/wCL + /qoAi/4XAIv+AACL/gAAi/4XAIr+qgCK/v8Aiv7/AIn+/wCI/v8Ah/7/AIf+/wCG/v8Ahf7/AIX+/wCE + /f8Ag/3/AIP9/wCB/f8Agf3/AID9/wB//f8Afv3/AH39/wB9/f8Ae/3/AHr9/wB5/f8AeP3/AHf9/wB3 + /aoAdvwXAHb8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4/QAAeP0HAHn9agB6 + /ewAev3/AHz9/wB9/f8Aff3/AH/9/wB//f8AgP3/AIH9/wCC/f8Agv3/AIT9/wCE/f8Ahf7/AIb+/wCG + /v8Ah/7/AIj+/wCI/v8Aif7/AIr+/wCK/v8Aiv6qAIv+FwCL/gAAAAAAAAAAAACK/gAAiv4XAIr+qgCK + /v8Aif7/AIj+/wCH/v8Ah/7/AIb+/wCF/v8Ahf3/AIT9/wCD/f8Ag/3/AIH9/wCB/f8AgP3/AH/9/wB+ + /f8Aff3/AH39/wB7/f8Aev3/AHn9/wB4/f8AeP2qAHf9FwB3/QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB5/QAAeP0CAHr9XAB7/ekAe/3/AH39/wB+/f8Af/3/AH/9/wCA + /f8Agf3/AIL9/wCC/f8AhP3/AIT9/wCF/v8Ahv7/AIb+/wCH/v8AiP7/AIj+/wCJ/v8Aiv7/AIr+qgCK + /hcAiv4AAAAAAAAAAAAAAAAAAAAAAACK/gAAiv4XAIn+qgCJ/v8AiP7/AIf+/wCH/v8Ahv7/AIb+/wCF + /f8AhP3/AIP9/wCD/f8Agf3/AIH9/wCA/f8Af/3/AH79/wB9/f8Aff3/AHv9/wB6/f8Aef3/AHn9qgB4 + /RcAeP0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB6 + /QAAdf0BAHv9VgB8/egAfP3/AH79/wB//f8Af/3/AID9/wCB/f8Agv3/AIL9/wCE/f8AhP3/AIX+/wCG + /v8Ahv7/AIf+/wCH/v8AiP7/AIn+/wCJ/qoAiv4XAIr+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACJ + /gAAif4XAIn+qgCI/v8AiP7/AIf+/wCG/v8Ahv7/AIX9/wCE/f8Ag/3/AIP9/wCB/f8Agf3/AID9/wB/ + /f8Afv3/AH39/wB9/f8Ae/3/AHr9/wB6/a0Aef0YAHn9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB7/QAAc/0AAHz9VQB9/egAff3/AH/9/wB/ + /f8AgP3/AIH9/wCC/f8Agv3/AIT9/wCE/f8Ahf7/AIX+/wCG/v8Ah/7/AIj+/wCI/v8Aif6qAIn+FwCJ + /gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACI/gAAiP4XAIj+qgCI/v8Ah/7/AIb+/wCG + /v8AhP3/AIT9/wCD/f8Ag/3/AIH9/wCB/f8AgP3/AH/9/wB+/f8Aff3/AH39/wB7/f8Ae/21AHr9HAB6 + /QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAB8/QAAdP0AAH39VQB+/egAf/3/AH/9/wCA/f8Agf3/AIL9/wCC/f8AhP3/AIT9/wCF + /v8Ahf7/AIb+/wCH/v8AiP7/AIj+qgCI/hcAiP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAACI/gAAiP4XAIf+qgCH/v8Ahv7/AIb+/wCE/f8AhP3/AIP9/wCD/f8Agv3/AID9/wCA + /f8Af/3/AH79/wB9/f8AfP3/AHz9wwB7/SYAe/0AAHr9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB9/QAAdf0AAH79VQB/ + /egAf/3/AID9/wCB/f8Agv3/AIL9/wCE/f8AhP3/AIX+/wCF/v8Ah/7/AIf+/wCI/qoAiP4XAIj+AAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/gAAh/4XAIf+qACG + /v4Ahv7/AIT9/wCE/f8Ag/3/AIP9/wCB/f8AgP3/AID9/wB//f8Afv3/AH39/wB8/dQAfP02AH39AAB7 + /QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+/QAAdv0AAH/9VQB//egAgP3/AIH9/wCC/f8Agv3/AIT9/wCE + /f8Ahf7/AIX+/wCH/v8Ah/6qAIf+FwCH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACG/gAAhv4VAIb+oACG/vsAhP3/AIT9/wCD/f8Agv3/AIL9/wCA + /f8AgP3/AH/9/wB+/f8Aff3gAHz9RgB+/QAAfP0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/ + /QAAd/0AAID9VQCA/egAgf3/AIL9/wCC/f8AhP3/AIT9/wCF/v8Ahv7/AIb+rQCH/hcAh/4AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACG + /gAAhv4SAIX+kQCE/fYAhP3/AIP9/wCC/f8Agv3/AID9/wCA/f8Af/3/AH795wB9/VAAjv0AAH39AAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACA/QAAfP0AAID9VQCB/egAgv3/AIL9/wCE + /f8AhP3/AIX+/wCF/rUAhv4cAIb+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACF/QAAhf0NAIT9fACE/fAAg/3/AIL9/wCC + /f8AgP3/AID9/wB//egAfv1UAHj9AAB+/QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAACB/QAAff0AAIH9VQCC/egAgv3/AIT9/wCE/f8Ahf3DAIX+JgCF/gAAhv4AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAACE/QAAhP0HAIT9aQCD/ewAgv3/AIL9/wCA/f8AgP3oAH/9VQB7/QAAf/0AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACC/QAAgv0AAIL9VQCD + /egAhP3/AIT91ACF/TYAhPwAAIX+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACD/QAAhP0CAIP9XACC + /ekAgv3/AIH96ACA/VUAgP0AAID9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACC/QAAev0AAIP9VwCD/coAhP1HAIT9AACE/QAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACC/QAAgv0BAIL9VwCC/dIAgf1WAHj9AACA/QAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACD + /QAAg/0DAIP9FACE/QMAhP0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACC + /QAAgv0DAIH9FgCB/QMAgf0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////////////////////////////////////////// + ///////xj///////////////////4Yf//////////////////8GD//////////////////+AYf////// + ////////////BDD//////////////////ggYf/////////////////wQDD/////////////////4IAYf + ////////////////8EADD////////////////+CAAYf////////////////BAADD//////////////// + ggAAYf///////////////wwAADD///////////////4YAAAYf//////////////8MAAADD////////// + ////+GAAAAYf//////////////DAAAADD//////////////hgAAAAYf/////////////wwAAAADD//// + /////////4YAAAAAYf////////////8MAAAAADD////////////+GAAAAAAYf////////////DAAAAAA + DD////////////hgAAAAAAYf///////////wwAAAAAADD///////////4YAAAAAAAYf//////////8MA + AAAAAADD//////////+GAAAAAAAAYf//////////DAAAAAAAADD//////////hgAAAAAAAAYf/////// + //wwAAAAAAAADD/////////4YAAAAAAAAAYf////////8MAAAAAAAAADD////////+GAAAAAAAAAAYf/ + ///////DAAAAAAAAAADD////////hgAAAAAAAAAAYf///////wwAAAABgAAAADD///////4YAAAAA8AA + AAAYf//////8MAAAAAfgAAAADD//////+GAAAAAP8AAAAAYf//////DAAAAAH/gAAAADD//////hgAAA + AD/8AAAAAYf/////wwAAAAB//gAAAADD/////4YAAAAA//8AAAAAYf////8MAAAAAf//gAAAADD////+ + GAAAAAP//8AAAAAYf////DAAAAAH///gAAAADD////hgAAAAD///8AAAAAYf///wwAAAAB////gAAAAD + D///4YAAAAA////8AAAAAYf//8MAAAAAf////gAAAADD//+GAAAAAP////8AAAAAYf//DAAAAAD///// + gAAAADD//hgAAAAB/////8AAAAAYf/4QAAAAA//////gAAAADH//4AAAAAf/////8AAAAAP//+AAAAAP + //////gAAAAD///gAAAAH//////8AAAAA///8AAAAD///////gAAAA////gAAAB///////8AAAAf///8 + AAAA////////gAAAP////gAAAf///////8AAAH////8AAAP////////gAAD/////gAAH////////8AAB + /////8AAD/////////gAA//////gAB/////////8AAf/////8AA//////////gAP//////gAf/////// + //8AH//////8AP//////////gD///////gH//////////8B///////8D///////////g////////h/// + ////////4P///////wf//////////8D///////4D//////////+Af//////8Af//////////AD////// + +AD//////////gAP//////AAf/////////wAB//////gAD/////////4AAP/////wAAf////////8AAB + /////4AAD////////+AAAP////8AAAf////////AAAB////+AAAD////////gAAAP////AAAAf////// + /wAAAB////gAAAD///////4AAAAP///wAAAAf//////8AAAAB///4AAAAD//////+AAAAAf//+AAAAAf + //////AAAAAP///wAAAAD//////gAAAAD///+AAAAAf/////wAAAAB////wAAAAD/////4AAAAA////+ + AAAAAf////+AAAAAf////wAAAAD/////AAAAAP////+AAAAAf////gAAAAH/////wAAAAD////wAAAAD + /////+AAAAAf///4AAAAB//////wAAAAD///8AAAAA//////+AAAAAf//+AAAAAf//////wAAAAD///A + AAAAP//////+AAAAAf//gAAAAH///////wAAAAD//wAAAAD///////+AAAAAf/4AAAAB////////wAAA + AD/8AAAAA////////+AAAAAf+AAAAAf////////wAAAAD/AAAAAP////////+AAAAAfgAAAAH/////// + //wAAAADwAAAAD/////////+AAAAAYAAAAB//////////wAAAAAAAAAA//////////+AAAAAAAAAAf// + ////////wAAAAAAAAAP//////////+AAAAGAAAAH///////////wAAADwAAAD///////////+AAAB+AA + AB////////////wAAA/wAAA/////////////AAAf+AAAf////////////4AAP/wAAP/////////////A + AH/+AAH/////////////4AD//wAD//////////////AB//+AB//////////////4A///wA////////// + /////Af//+Af//////////////4P///wP///////////////H///+H///////////////x////x///// + /////////////////////////////////////////////4lQTkcNChoKAAAADUlIRFIAAAEAAAABAAgG + AAAAXHKoZgAAF4FJREFUeNrtnX3sZcVZxz8XFhbYUCDQLS00LWC0bNtEV02bCtgmhkK0JjXQqK0GoSS+ + xTambUyNmhgxsbYxaKx/CKJJSdPiW1DbUv6wUAixQvAPtkWLW2ylXQTaRbuUl909/nG4/d23c8+cc2bm + eZ6Z55PA/u69c848M/N8vzPn5Z47a5oGx3Hq5ATpABzHkcMNwHEqxg3AcSrGDcBxKmZXqh0f/flZ+8eL + /2z8d+BnsyHbj6lz4d/Z0H2PLbPlvVlXuW1/D/lsTrPldTOwXLPwz5bPg99rxpdttn0eWk/Ats3Y/Q/8 + bNet8U/Y+wpADw3wiHQQmdvrl6C6mE3fRQi2DWBM+uhMuXlUFwH3SgfTSZykPInt6wpzZNJqEmwbQBms + CuBNwCekg0rEGcDzAX3gZEKvAVhJiWlxdm39DuAG6aZF5nzg8Ii+SIPlaTsieg2gfPoS/oPAtdJBDmaz + sL4f+FrA1lZsvxjcAGQITfSbafgx6WAn8lbgwQHlyzABIyuMeg1ALs2G1nwnsE8s2mlcB3xmxHbdfWRE + WFao1wBkGGs7B4C90sEP5AbgpgnbH88SZeWGkuxGIBqid26CXeZk6prjcWAP8MzGPevqmFuBn524jxnw + HLBbujEbIyvjQCWhAZTOMNHFSpcjtKs2zel3F3BZpH2dDDwJnCPdqFLxQ4D0UopdQ56l8Ti+SDzxzzkb + +LJ0w0pF1gBiSUPvfNgkii98j/n65hBwcW+pcYcq3wPck60luVBw2OYrgHSklt64/aeJ6gjwssTt/RHK + vUNSDDeADprBH4wolTDMjBwFTstUV4l3SG4n8SpBvwFoSHHdEUv2UAOcmLlOm3dIKkW/AeQijoykxChR + r+S9+zdD5DskFRyPS+AGEI+81xOarZ/ab2s/dwKvlQ7COm4AqzQD3w/5VD56a3WE8hDpTz4WjRtAH9vT + XeM9YSnjSbPvacvvQ+Q7CTk8buWHFm4AXfSn+snovSknhVC1Gd0iR8h/MrII3ADGcQbtfeqaiSPY2YR9 + 5Z39jmatrRDSG0B5p6ZexfYn22giRo9onvktx7qD4GGCrwCGsR94VDqIgUwRxehtBXN6mgkoP2aPjRtA + OD8OPCAdxEjKeX5y+bFvJpEx6TAA/cP1S8A/SgcxkSG9rH9E6mhDcnQYgG4+DHxUOohIhIiiJOGU1JYk + 2HkgiMxTb24DrsoWc542bqulRMHoe16SIuwYQH7upf2RjjTIpuWm2nWKP86tVm4CHbgBbOYR2p/pKplF + UegUf7r2xkXj/aCBuAGs8yTtY6hqwGjaTmqvrwQWcANY5jvAKdJBZKSmFcCcMBMwPKsPoUwDGOfzx0dt + ZZfZyt8VpPt3yb8SUNrDfhmwpbal4SzwvZJRKMf8uAHUlwizkZ+VSG1jv0Y+A9DZ1TqjSkcB32CPjp4c + EOj5mlcAegY+D0PSy02gEmo1gNoGfIyg3QQqQJcB5BmC2gZ6ipDdBArvAV0GEBvZJ+dqIEb6Fi6BNarK + EXsGMH54qhpY4grXTaBQ7BnAOKoZ0BdJIVg3gQKpwQDyD6Rs6qQUatx967eUYSOpvz1rlG4AVbj4AjlS + cFwdQ7fSI6aic6hkAyh64DaQUzJ65JmHYnOpVAModsA6kBCkm8AYlPVaiQbg4q+jbgmKy63SDKAZ8K56 + AsLWIEANMeTEaDZtJr8BpOu+ogYmgG7h5e+J8k1guYVpezhjb5ayAihP/P2/SqwNjTGlpIics2sAzYa/ + 6kCz0HTFlucR66bRaQDh3WpvAMr/5Tr5GPNGYC8HF9BpAGGY7vgRyAtLe6yzge/Hw2wuWjUAsx0+Ep3i + n3X8rTnmdJjMSYsGoPf7gGn2b1lIumL3lcAa1gzAXAdPRJeA6m3DEBpLLbZkAC5+u6Rty2zQ2zkwk6tW + DMBMh0Yi/49WpK+9JEMLwUTOyhlAyZf6hrLcwpKFEvaTXPJRxNq2O3eVjLL2FUD54l9GSVoYaaON3lKd + w5oNQHXHJcBGOntbxzAulzP0klYDSHuAoM9a7Aiitvk7XmP0ZR06DUBlRyWkKCF427eiLre1GYC6Dkoc + dW0C2IRcH8jU/IJYezegyQBq+xmPcsU/vGXl9sU6u4DD0kHM0WIAZUt9nVgXk0pCSy7Go3uUzwC+Kh0e + 6Oh0ufSWqbmm2W4IDXBqUMkyevCVwAPSQUgbQB1z2w5lpG46ngX2SgeRkf3A7ZIBpDWAZsKnMTfSQVrx + G+6YFZ4Avi9rjbI/nv424Mas7V1AagVQRrqGt8Jn/mH8B3CZdBAZ+bUX/8uOhAHoE38xz3gtis8D75IO + IiM30q4GspLbANJd6tNnK7Aofsn4tPVNuCXeCvyOdLhJWe6L22nPC2QjpwFoS8PU6J35bY3E7wJ/JR1E + Rh6gvUKQhVwGYCvl+oLe3ppjaBa/Ta4BPicdREa+SnuvQEvCbMphANPFb8c+nqG908uJz1uAh6WDyMhh + MuRSagP4y9QNiMo0o3kc2CPdhKKZcTHtZcJU+9dG8u8NpDaAa4C7UjdCAV8Czs1SU9Pxdz3spV1p1ULS + Uc5xCPBm4N8z1BOPZtD7dwH7pEOujD3UY39J1yW5TgK+hpRLt9R0p9rHaA3OmcrwNJe+jT087vESTn5Q + krMT99Le622H7XPM7wE/Jx1i5eg7ajfWttxnrE8FjudqXEKuBW6RDiIbM8YvuKdsq6WG/GTTRx4DaJaa + dAIpB2y5rhRcDtyZtAZnKPZNYKcFWSdHqWvWVgfs9cBD0kFYIfMgx6tOLjuzr4wlb1qxZgLnAV+XDsLZ + irWcWo09O9J3re0MWPql+xROB74tHYQThDUTaBC8oiFtAKB/wHbR3t/v2EF7Ts15DjhFMgAd11L1zv0z + pop/aBpaSFsbaM2pOU8hLH7QYwAQMmB5xZE+gVzsYSi+kWYkB4FzpIMAXQYAegZMSxzOdPKMZXgt/wJc + JNQXa2gzAJAXn3T9Tny0jOnfAW+UDmIRjQYAcgOmJVGc+Ej/XtGNwE9Jd8IqWg0Apnb/8ONrF3/5SI3x + e4D3aswwDZcBt5Hrco7CoXESkfsS4duBv5dudBfaDQDSD5iLvz5ymcAbgC9IN3YbeQ1g/N1+qQbMxV8v + qU3gQuAr6qJawcIKYE7srnHxO6nkdjbwTenGhWDJACDegLn4nTmxTeAU2lt8TaDPAPoPE6YOmIvfWWV7 + ToVnXNpnXSRA82XAbYSLuBm5nVMbU3PDyheQlrBpAOOenOLid/oYmyNmc8umAewQ2vFmB8jJTlUTi3UD + gP4BkBkgc4tBIXTKZ/zEorM9nZRgANDd7caGo3Bsza06J5bIlGIAUMiAOKoofmIpyQBgfWB8Ie5MJb7Y + FdlHaQbQBL7nOKGcuuG9YnKqJANotn5mdMiMhl0KL6H7l4iLGBoZA4jfdSF7LGLAnGycDTy99M760j1d + TmU6TChhBTBkEGyZwJRobbVUGy8Hngwsa7qnrRvAmM43PWDOQIbPpK9m+C9Amc0pvQbQ36VTjutlB8xs + uhhi3BL6YsZ8h7/F5KjqNYDtxOhskwPm9DD+yv1+4IsTazeXUxYNIGYnmxswdcw6/rbFJcADkfZlKqes + GcD0zm0C3pGN0EnJukldAXw+ci1mssCSAaTsVDMD5kTlKuDTSfY8s5FT6QzA3rxqYsCcDmaD3gb4BeC2 + xFGpzykLK4Ccnah+wJIx63ldFu8F/iJTXapzStYAQi71aYyqnFp1ktZ8fhv4o0l1DN928+gqMFl9DwXd + QVIS43/BwNHMR4BfF6pbZU5pNQAN86HKAXNWCH8U503AdcLRqsspjQYwXPxjujVsG3UDpgJ7z7/9JHB1 + 6koCu2VYTiXua20GoDGtjqKvn5xwPgVcKR3ECmomFk2JrVH8ACfS/tLL7t7oVQyps8BdwGVbS8itZlRk + jJbLgM2IT3JyMnBEOghTiKc299MnfnnEs3vWNNNieOFnZjuDPf9z4fXS35v/bQLKbK8jvK7B28yWXx9m + xlkbyw+pd+B7s5BtVv8e8tmcZsvr0M9W/m66Ph/6XjOo7AFg3/x1E7Kv0Pp6tmnG7H+ugMC6Vus46ePj + NSy9AhB3wIERnAl8Qzpkh20rjP8E9kmHNxAxHUgagLz4x3EucFA6CGcjjwEXSgcxEhE9pDeAZsC7k3aZ + lQuAh6SDWENBxwjyBPCK0VuPOWcR/zxH9hGUWAGUkqavBe4rpjW2OQycIx1EJLJmVG4DKE0ubwTukA6i + co4AZ0gHEZlsOslpAOkv9ck8RfdyQr9WWpr9TSHO8vk54DTppkxmc19kyZZcBlB66l9Fe6+5Xpqe1/Y4 + Snt/RskkH6UcBmA61ZrwD64DPiQdbyUcp71DswbaTEt0Y1VqA4gnfhs28v4X/3PSoeIWWoE2JyG1Afxb + 4v3HZ3pXfwj5r52WSpNc+pVZS2oD+AHgy9KNFOAm2vMCadh2i2651NPSdZLZUo5zAN8L/HeGeuLSDHx/ + ndtorxA403HxJyLXVYBXAk9lqisN41LwDtp7BZwQBC+HJYh7eJlYWw0glgEsfl+pi3OAb6duUHSmp999 + tHcNOsPRL/50zHo+CNFcLzEMYMgR6enA8xHqtMZDtN8fsEvep+iCi38b0c4CxTCA1WD7vv6zGzgWod5u + dKbOQdpvEjr96BzB1MwW/t9Ns3GrkcQ5BGgCTWCHXQFlSuQbtM8UqIYR2akjL2QuB2YVP8Q9CTjUBKQf + RiLFtyjh/vU06BC/DNnFD/FFONQEZoNKl8MRyr+PfSj1jP464eIPO0wIJsUsPM0E6uE56rmfvQ8XfzdJ + Zv45qZbhbgJhrT86sHyJ1NXaZUTFD2mPw9OYQGnp0hTXomGtD6W8KUJc/JD+RJyvBMKo0QRqbPMcFeKH + PGfi3QTCqEkQZbY1LHPViB/yXYpzEwijTGFYbmPcTFQlfsh7LT6/CdhKNdtRe9v66LuAl138ENsA+ofX + VwJhlCiUEtsUirqZf47E3XhuAmGUJJiS2jIUteIHCQPY/IhDN4HNlCCcEtowlnHiz5jtkvfjuwmEYVlA + lmOfiuqZf470F3LcBMKwKCSLMccivfgjKUHaADY1pe/eODcB/ViKNYzwrDMx88/RYACbOiHuSqCcdLTQ + EgsxpsKU+EGPAYAfDoSiWWCaY0uNOfGDLgOAISaQ9AeT1KNRaBpjyoVJ8YM+A4CSVgJpJaFJcNNi0TuC + 46Lf/pgbVa3VaABQkgmkRYMJDIth6EjpHlmzM/8crQYAbgKhSJqABgOSwrz4QbcBgJtAKBJCdPF30y9+ + JZmaxgDipoabQBg5Beni78bEzD9H+wpgThwTMJq2A8LO0UKjvRiFosQPkgYwPI18JRBGSoG6+HN+nz/d + j45+FysrgK7mlm8C4yTXTNg2biRlUNzMPye6AWTIkvpMYBwxh8K2+KdlQLHiB+0rgO60m62UsWcCeSQV + oxa74k+/CDctftBuANvxlUAYUwRsU/yzge+PK21e/GDbAMBNIJQxQh73ox1l9HAV4gf7BgBjTCD1vKZz + 3hwSlc4WxGI24dOCxA9lGAD4SiCUEGGXLf7txBW/gSxLawB5U8lNIIxm5Gf6mQ16e2ixomb+OaWsAOa4 + CYTRbHyv1t6oVPygwQDizzl2TSDv/Nt0/L2DZM/kqbsJqCnto7uFs0/eANJg1wTy0mB92T+eY8x687/Y + mX9OqQYAbgL1MHzkngd29ZQpXvxQkgGELWKbnm2KHOQk2O2pZ4DdPWVGiz9JtyTs63IMoBtfCThzngb2 + 9JSpYuafU4MBQC4TqPVo2gZPAGdu/GT1myWbPikUGwYQR1i+EsiNnh58DNjbU6Y68YMVA4iHbRPwFcYY + DgLn95RJK35dWbREbQYA/luE5dA/MgeAi3rKVDnzz6nRAMD6SsAJ4X7gdT1lqhY/GDaACKvhMk3ADxMA + 7gZ+uKfMuvjHjLCNrOgkjwHoTcoyTaBu/gn40Z4y1c/8c8yuACIy3QT0GlxtfAL4iZ4yLv4FphtArOSX + 3c9sZdt0KwE3ix3iSu8m4Kd7yugVv1AkvgLYofzDgabntV0+Alw/sPX2xi8BbgDLbDSBTp3U/A16PfwW + 8L6eMmWIP0HUfd+I0kXIt7enM2P9u/KzAeWdfLwH+OOeMmWIPxG2DCAXDbOVR4e6CWii7e1rgVt6So4T + f0Wj6QbQje6VwGI0eVZGmrgK+JueMj7zB+AGsI2ulUC34CqaO8S4Arijp4yLPxA3gH52RN0n/9XyznC2 + 996lwD09e9ArfoWZ4QYQgp8T0MB+4MGeMnLizzzisapzAwhH9zkB7UzrjYuBh3vK6J35FZPXAHKerEpT + V90mINOaC4BHe8qUI/7MfRzPAOo5E123CYxgQge8HDjUUyad+CsYOT8EGIebQHrOBr7ZU6acmV8IfbcC + 25FJ+d8dkOMluPi7idhSXwFMw1cC8TkVeLanTL3ij4y+FYA9wlYCTWd5esrXxMm4+LNi0wD0iWPa4YC+ + 9kiwC3hha4mZiz82Ng1AJ35OYDwnAMd6yrj4E+AGEBc3geGEnBexI369kW3EDSA+bgLhhLRdXvwFj1D5 + BiBzfO0m0I8N8Uu1PBPxDcBPaM1xE5jW1jLFr4zyVwCyuAkMb+OmcwI19IsIMgZQ1yrBTSC8bbuA4wO3 + KZNMrfYVQB7cBPrbdArr9wGU2A+qcAPIR80m0NeW04HvFNx+teg1gDIPE2o0gb42nA38r4l2a4sqQjx6 + DSA2egylJhPoi/0VwJMFtdcc9RiALmowgb6YLwAeK6CdprFtAHpm9TGUbAJ9se4DDhpuXzHYNgALbJd1 + iSbQF+MPAQfWtrHQsgJxA5CnJBPoi+3NwL+ulOpvj+YWG4/ZDUAHJZhAX0xvA/55pY0a21EVbgB62GQC + fY8X00JfLO8Ebl94fRTPPRUjmG4QbJ+gk2I1JY6zfYwUpFBvDL8MfGzh9bPASdJBOy31ubB+Y1oV1DFg + 15awJR/s2if+DwJ/uvD6/2gf+ukoQdYA9ItRilVhvQDs7ih7bMtnOWNc5Q+BGxZeP0X7uG/75Fx3Ja6r + vhWAHXaGvjXKZ4HTOso+D+zZ+Ekak+1Lyz8H3rfw+uvAOUkicSaRxAB8Yo/GqtCOAGd0lH0GOEsgplU+ + Cbx74fVB4LwMcU2LulL0rwBC3MSq44TFvZq6h4GXdpQ9TPt7eqnok9GngKsXXh8ALkoYz/SIK0e/ATiw + nsb/A5zfUfYQcGGGGFa5G7hy4fUXgNdl6Z0h0bohLOEGYIfV1P0a3bPrV4DXJ6x7lQeYcelC6c8Bb8jc + P84I6jWA1IcNeU6+PUL7xZpNPAS8KUGdq3wJ2L/w+h+AtyRpfUx8JQDUbAB2WU3dA8APdpS9D3hrxLpW + eRR4zcLrW4GflOwcdSg3mvQGYPUEnW5W0+p+WFiCL/NZlk/Mja1jlUPAqxZefxR4l3THdEWtXIdi+ArA + LquPz74buKKj7F8D1wfvtV8v3wJetvD694Ffke4QZzg6DMBXCTsM64vVH9X8NHBVR9mbgN8I2Gffl3WP + AGcuvH4/8JtJ+kL7tB0rPsF26jAAZwq7aO8EnHMbcE1H2T+gvUW3i75UfI7luxGvBz4s3QGT0G4yiXED + KIPdtHcCzrkF+NWOsh8Abt7wfp8UjgInL7x+B+2qQgeVC3ksZRlA3YcSe4CnF17/Ce238TbxbtrzAnP6 + 5HMcOHHh9ZXAbS66zSTplkR9XZYBjKEs0ziT5cds30C77N/E1bRp1Zdaqw8muRT4jHRDnTi4AcRG3lBe + SvvtuzkfAP4sUmv2A/dINzAJla5m7BiAvLAscR7t7cBzfpHlp/KEsNrjFwMPDo6kUmFZwY4BOC3hRngh + 7W26c94J/O3IWl4NPCzddCc+bgBzylxh7GN51n47cMfAnjgX+C/phnRiZYWhNE43gPLZD9y78PryLWUv + WXl9FvC4dAOcdJg2gDIn7Qisd8wltN8JgO1z0T3sfHloD+0DRupizEytdHYPId8TZf1nIKQJ/VbgZ/GR + qoZZ0/g86ji1YvoQwHGcabgBOE7FuAE4TsX8Px1JqwQdRpdiAAAAAElFTkSuQmCC + + + \ No newline at end of file diff --git a/src/Forms/PasswordInput.Designer.cs b/src/DAZ_Installer.Windows/Forms/PasswordInput.Designer.cs similarity index 99% rename from src/Forms/PasswordInput.Designer.cs rename to src/DAZ_Installer.Windows/Forms/PasswordInput.Designer.cs index 5359018..dfc0527 100644 --- a/src/Forms/PasswordInput.Designer.cs +++ b/src/DAZ_Installer.Windows/Forms/PasswordInput.Designer.cs @@ -1,5 +1,5 @@  -namespace DAZ_Installer +namespace DAZ_Installer.Windows.Forms { partial class PasswordInput { diff --git a/src/Forms/PasswordInput.cs b/src/DAZ_Installer.Windows/Forms/PasswordInput.cs similarity index 69% rename from src/Forms/PasswordInput.cs rename to src/DAZ_Installer.Windows/Forms/PasswordInput.cs index bb75d7f..de6ae79 100644 --- a/src/Forms/PasswordInput.cs +++ b/src/DAZ_Installer.Windows/Forms/PasswordInput.cs @@ -2,26 +2,16 @@ // You may find a full copy of this license at root project directory\LICENSE using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows.Forms; -namespace DAZ_Installer +namespace DAZ_Installer.Windows.Forms { public partial class PasswordInput : Form { protected internal string password; public string archiveName = string.Empty; public string message = string.Empty; - public PasswordInput() - { - InitializeComponent(); - } + public PasswordInput() => InitializeComponent(); private void submitBtn_Click(object sender, EventArgs e) { @@ -34,20 +24,15 @@ private void PasswordInput_Load(object sender, EventArgs e) if (message.Length == 0) { mainLbl.Text = $"{archiveName} is password-protected. Please enter password to decrypt file."; - } else + } + else { mainLbl.Text = message; } } - private void PasswordInput_FormClosing(object sender, FormClosingEventArgs e) - { - password = maskedTextBox1.Text; - } + private void PasswordInput_FormClosing(object sender, FormClosingEventArgs e) => password = maskedTextBox1.Text; - private void cancelBtn_Click(object sender, EventArgs e) - { - Close(); - } + private void cancelBtn_Click(object sender, EventArgs e) => Close(); } } diff --git a/src/Forms/PasswordInput.resx b/src/DAZ_Installer.Windows/Forms/PasswordInput.resx similarity index 100% rename from src/Forms/PasswordInput.resx rename to src/DAZ_Installer.Windows/Forms/PasswordInput.resx diff --git a/src/Forms/ProductRecordForm.Designer.cs b/src/DAZ_Installer.Windows/Forms/ProductRecordForm.Designer.cs similarity index 99% rename from src/Forms/ProductRecordForm.Designer.cs rename to src/DAZ_Installer.Windows/Forms/ProductRecordForm.Designer.cs index c3878a8..859f9b2 100644 --- a/src/Forms/ProductRecordForm.Designer.cs +++ b/src/DAZ_Installer.Windows/Forms/ProductRecordForm.Designer.cs @@ -1,5 +1,5 @@  -namespace DAZ_Installer +namespace DAZ_Installer.Windows.Forms { partial class ProductRecordForm { @@ -93,7 +93,7 @@ private void InitializeComponent() // this.thumbnailBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); this.thumbnailBox.ContextMenuStrip = this.thumbnailStrip; - this.thumbnailBox.Image = global::DAZ_Installer.Properties.Resources.NoImageFound; + this.thumbnailBox.Image = global::DAZ_Installer.Windows.Resources.NoImageFound; this.thumbnailBox.Location = new System.Drawing.Point(515, 25); this.thumbnailBox.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); this.thumbnailBox.Name = "thumbnailBox"; diff --git a/src/Forms/ProductRecordForm.cs b/src/DAZ_Installer.Windows/Forms/ProductRecordForm.cs similarity index 85% rename from src/Forms/ProductRecordForm.cs rename to src/DAZ_Installer.Windows/Forms/ProductRecordForm.cs index 11f9cda..02f6e8c 100644 --- a/src/Forms/ProductRecordForm.cs +++ b/src/DAZ_Installer.Windows/Forms/ProductRecordForm.cs @@ -1,32 +1,32 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE +using DAZ_Installer.Core; +using DAZ_Installer.Database; +using DAZ_Installer.Windows.Pages; +using DAZ_Installer.Windows.DP; using System; using System.Collections.Generic; -using System.IO; -using System.Drawing; -using System.Windows.Forms; -using DAZ_Installer.DP; -using System.Threading.Tasks; -using DAZ_Installer.Utilities; -using DAZ_Installer.Properties; -using DAZ_Installer.Forms; using System.Diagnostics; +using System.Drawing; +using System.IO; using System.Linq; +using System.Windows.Forms; +using DAZ_Installer.IO; -namespace DAZ_Installer +namespace DAZ_Installer.Windows.Forms { public partial class ProductRecordForm : Form { private DPProductRecord record; private DPExtractionRecord extractionRecord; private uint[] maxFontWidthPerListView = new uint[5]; - private HashSet tagsSet = new HashSet(); + private HashSet tagsSet = new(); public ProductRecordForm() { InitializeComponent(); fileTreeView.StateImageList = Extract.ExtractPage.archiveFolderIcons; - if (DPGlobal.isWindows11) + if (DPGlobal.isWindows11) applyChangesBtn.Size = new Size(applyChangesBtn.Size.Width, applyChangesBtn.Size.Height + 2); } @@ -34,14 +34,14 @@ public ProductRecordForm(DPProductRecord productRecord) : this() { InitializeProductRecordInfo(productRecord); if (productRecord.EID != 0) - DPDatabase.GetExtractionRecordQ(productRecord.EID, 0, InitializeExtractionRecordInfo); + Program.Database.GetExtractionRecordQ(productRecord.EID, 0, InitializeExtractionRecordInfo); } public void InitializeProductRecordInfo(DPProductRecord record) { this.record = record; productNameTxtBox.Text = record.ProductName; - authorLbl.Text += string.IsNullOrEmpty(record.Author) ? "Not detected" : record.Author; + authorLbl.Text += string.IsNullOrEmpty(record.Author) ? "Not detected" : record.Author; tagsView.BeginUpdate(); Array.ForEach(record.Tags, tag => tagsView.Items.Add(tag)); tagsSet = new HashSet(record.Tags); @@ -54,7 +54,7 @@ public void InitializeProductRecordInfo(DPProductRecord record) dateExtractedLbl.Text += record.Time.ToLocalTime().ToString(); CalculateMaxWidthPerListView(); UpdateColumnWidths(); - DPDatabase.ProductRecordModified += OnProductRecordModified; + Program.Database.ProductRecordModified += OnProductRecordModified; } @@ -96,13 +96,13 @@ public void InitializeExtractionRecordInfo(DPExtractionRecord record) private void browseImageBtn_Click(object sender, EventArgs e) { - OpenFileDialog dlg = new OpenFileDialog(); + var dlg = new OpenFileDialog(); dlg.Filter = "Supported Images (png, jpeg, bmp)|*.png;*.jpg;*.jpeg;*.bmp"; dlg.Title = "Select thumbnail image"; if (dlg.ShowDialog() == DialogResult.OK) { var location = dlg.FileName; - + if (File.Exists(location)) { try @@ -112,13 +112,15 @@ private void browseImageBtn_Click(object sender, EventArgs e) thumbnailBox.ImageLocation = location; thumbnailBox.Image = img; thumbnailBox.Show(); - } catch (Exception ex) + } + catch (Exception ex) { - DPCommon.WriteToLog($"An error occurred attempting to update thumbnail iamge. REASON: {ex}"); + // DPCommon.WriteToLog($"An error occurred attempting to update thumbnail iamge. REASON: {ex}"); MessageBox.Show($"Unable to update thumbnail image. REASON: \n{ex}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } - - } else + + } + else { MessageBox.Show($"Unable to update image due to it not being found (or able to be accessed).", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } @@ -153,7 +155,7 @@ private uint GetMaxWidth(ListView.ListViewItemCollection collection) for (var i = 0; i < itemsText.Length && i < 3; i++) { var width = TextRenderer.MeasureText(itemsText[i], collection[0].Font).Width; - if (width > maxWidth) maxWidth = (uint) width; + if (width > maxWidth) maxWidth = (uint)width; } return maxWidth + 20; } @@ -165,15 +167,15 @@ private void BuildFileHierachy() // Initalize the map by just connecting a folder path to a tree node. foreach (var file in extractionRecord.Files) { - var dirName = Path.GetDirectoryName(file + ".e"); + var dirName = Path.GetDirectoryName(file + ".e")!; // If it does not exist, we need to create tree nodes for this and add the root tree node to treeNodes. if (!folderMap.ContainsKey(dirName) && dirName.Length != 0) { // This is to ensure that the file doesn't get treated as a directory (EX: file doesn't have an ext) ReadOnlySpan folderSpan = dirName; - char seperator = PathHelper.GetSeperator(folderSpan); - int lastIndexOf = folderSpan.Length; - TreeNode lastNode = null; + var seperator = PathHelper.GetSeperator(folderSpan); + var lastIndexOf = folderSpan.Length; + TreeNode? lastNode = null; while (lastIndexOf != -1) { var slice = folderSpan.Slice(0, lastIndexOf).ToString(); @@ -184,7 +186,7 @@ private void BuildFileHierachy() folderMap[slice].Nodes.Add(lastNode); break; } - if (PathHelper.GetNumOfLevels(slice) == 0) treeNodes.Add(folderMap[slice]); + if (PathHelper.GetSubfoldersCount(slice) == 0) treeNodes.Add(folderMap[slice]); if (lastNode != null) folderMap[slice].Nodes.Add(lastNode); folderMap[slice].StateImageIndex = 0; lastNode = folderMap[slice]; @@ -211,16 +213,12 @@ private void BuildFileHierachy() foreach (var file in extractionRecord.ErroredFiles) { var parent = PathHelper.GetParent(file); - if (folderMap.ContainsKey(parent)) + if (!folderMap.ContainsKey(parent)) continue; + foreach (TreeNode node in folderMap[parent].Nodes) { - foreach (TreeNode node in folderMap[parent].Nodes) - { - if (node.Text == parent) - { - node.ForeColor = Color.DarkRed; - break; - } - } + if (node.Text != parent) continue; + node.ForeColor = Color.DarkRed; + break; } } var treeNodesArr = new TreeNode[treeNodes.Count]; @@ -242,19 +240,19 @@ private void NormalizeExtractionRecord() foreach (var file in extractionRecord.ErroredFiles) normalizedErroredFiles.Add(PathHelper.NormalizePath(file)); - extractionRecord = new DPExtractionRecord(extractionRecord.ArchiveFileName, - extractionRecord.DestinationPath, - normalizedFiles.GetInnerArray(), normalizedErroredFiles.GetInnerArray(), - extractionRecord.ErrorMessages, normalizedFolders.GetInnerArray(), + extractionRecord = new DPExtractionRecord(extractionRecord.ArchiveFileName, + extractionRecord.DestinationPath, + normalizedFiles.ToArray(), normalizedErroredFiles.ToArray(), + extractionRecord.ErrorMessages, normalizedFolders.ToArray(), extractionRecord.PID); } private void deleteRecordToolStripMenuItem_Click(object sender, EventArgs e) { - var result = MessageBox.Show($"Are you sure you want to remove the record for {record.ProductName}? " + + DialogResult result = MessageBox.Show($"Are you sure you want to remove the record for {record.ProductName}? " + "This wont remove the files on disk. Additionally, the record cannot be restored.", "Remove product record confirmation", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.No) return; - DPDatabase.RemoveProductRecord(record, OnProductRecordRemoval); + Program.Database.RemoveProductRecord(record, OnProductRecordRemoval); } private void OnProductRecordRemoval(uint id) @@ -269,14 +267,14 @@ private void OnProductRecordRemoval(uint id) private void deleteProductToolStripMenuItem_Click(object sender, EventArgs e) { - var result = MessageBox.Show($"Are you sure you want to remove the record & product files for {record.ProductName}? " + + DialogResult result = MessageBox.Show($"Are you sure you want to remove the record & product files for {record.ProductName}? " + "THIS WILL PERMANENTLY REMOVE ASSOCIATED FILES ON DISK!", "Remove product confirmation", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.No) return; uint deletedFiles = 0; // Try deleting at the destination path. if (!Directory.Exists(extractionRecord.DestinationPath)) { - var r = MessageBox.Show("The path at which the files were extracted to no longer exists. Do you want to check on through your current content folders?", + DialogResult r = MessageBox.Show("The path at which the files were extracted to no longer exists. Do you want to check on through your current content folders?", "Root content folder doesn't exist", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (r == DialogResult.No) return; } @@ -285,16 +283,16 @@ private void deleteProductToolStripMenuItem_Click(object sender, EventArgs e) { // Quick test. if (File.Exists(Path.Combine(extractionRecord.DestinationPath, extractionRecord.Files[0]))) - MessageBox.Show("None of the product files were removed due to some error.", "Failed to remove product files", + MessageBox.Show("None of the product files were removed due to some error.", "Failed to remove product files", MessageBoxButtons.OK, MessageBoxIcon.Error); else { - var r = MessageBox.Show("The path at which the files were extracted to no longer exists. Do you want to check on through your current content folders?", + DialogResult r = MessageBox.Show("The path at which the files were extracted to no longer exists. Do you want to check on through your current content folders?", "Root content folder doesn't exist", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (r == DialogResult.No) return; } - - foreach (var contentFolder in DPSettings.currentSettingsObject.detectedDazContentPaths) + + foreach (var contentFolder in DPSettings.CurrentSettingsObject.detectedDazContentPaths) { deletedFiles = DeleteFiles(contentFolder); if (deletedFiles > 0) break; @@ -307,7 +305,7 @@ private void deleteProductToolStripMenuItem_Click(object sender, EventArgs e) else if (delta > 0) MessageBox.Show($"Some product files failed to be removed.", "Some files failed to be removed", MessageBoxButtons.OK, MessageBoxIcon.Information); - else DPDatabase.RemoveProductRecord(record); + else Program.Database.RemoveProductRecord(record); } private uint DeleteFiles(string destinationPath) @@ -324,7 +322,7 @@ private uint DeleteFiles(string destinationPath) } catch (Exception ex) { - DPCommon.WriteToLog($"Failed to remove product file for {record.ProductName}, file: {file}. REASON: {ex}"); + // DPCommon.WriteToLog($"Failed to remove product file for {record.ProductName}, file: {file}. REASON: {ex}"); } } return deleteCount; @@ -346,8 +344,8 @@ private string[] CreateFinalTagsArray() for (var i = 0; i < tagsView.Items.Count; i++) tags.Add(tagsView.Items[i].Text); tags.Remove(record.ProductName); - var oldProductNameRegexMatches = DPAbstractArchive.ProductNameRegex.Matches(record.ProductName); - var newProductNameRegexMatches = DPAbstractArchive.ProductNameRegex.Matches(productNameTxtBox.Text); + System.Text.RegularExpressions.MatchCollection oldProductNameRegexMatches = DPArchive.ProductNameRegex.Matches(record.ProductName); + System.Text.RegularExpressions.MatchCollection newProductNameRegexMatches = DPArchive.ProductNameRegex.Matches(productNameTxtBox.Text); for (var i = 0; i < oldProductNameRegexMatches.Count; i++) tags.Remove(oldProductNameRegexMatches[i].Value); for (var i = 0; i < newProductNameRegexMatches.Count; i++) @@ -363,10 +361,10 @@ private string GetThumbnailPath() private void applyChangesBtn_Click(object sender, EventArgs e) { - var r = MessageBox.Show("Are you sure you wish up apply changes? You cannot revert changes.", "Confirmation", MessageBoxButtons.YesNo, MessageBoxIcon.Question); + DialogResult r = MessageBox.Show("Are you sure you wish up apply changes? You cannot revert changes.", "Confirmation", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (r == DialogResult.No) return; var p = new DPProductRecord(productNameTxtBox.Text, CreateFinalTagsArray(), record.Author, record.SKU, record.Time, GetThumbnailPath(), record.EID, record.ID); - DPDatabase.UpdateRecordQ(record.ID, p, extractionRecord); + Program.Database.UpdateRecordQ(record.ID, p, extractionRecord); } private void editTagsToolStripMenuItem_Click(object sender, EventArgs e) @@ -389,7 +387,7 @@ private void pasteNewTagsToolStripMenuItem_Click(object sender, EventArgs e) { var txt = Clipboard.GetText(); var tags = new List(txt.Split('\n')); - bool dismissedTags = false; + var dismissedTags = false; foreach (var tag in tags) { if (string.IsNullOrWhiteSpace(tag)) continue; @@ -408,7 +406,7 @@ private void pasteNewTagsToolStripMenuItem_Click(object sender, EventArgs e) } tagsView.EndUpdate(); if (dismissedTags) - MessageBox.Show("Some tags were not added due to the size of the text being greater than 80 characters.", + MessageBox.Show("Some tags were not added due to the size of the text being greater than 80 characters.", "Some tags omitted", MessageBoxButtons.OK, MessageBoxIcon.Warning); } @@ -423,7 +421,7 @@ private void removeTagToolStripMenuItem_Click(object sender, EventArgs e) tagsView.Items.Remove(c[i]); } tagsView.EndUpdate(); - + } private void tagsStrip_Opening(object _, System.ComponentModel.CancelEventArgs __) @@ -443,7 +441,7 @@ private void replaceToolStripMenuItem_Click(object sender, EventArgs e) } if (txt[0].Length > 70) { - MessageBox.Show("Replace failed due to text being longer than 70 characters. Make sure the text in your clipboard is no more than 70 characters.", + MessageBox.Show("Replace failed due to text being longer than 70 characters. Make sure the text in your clipboard is no more than 70 characters.", "Too many characters", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } @@ -463,7 +461,8 @@ private void copyToolStripMenuItem_Click(object __, EventArgs _) try { Clipboard.SetText(string.Join('\n', a)); - } catch { } + } + catch { } } private void tagsView_KeyUp(object sender, KeyEventArgs e) @@ -480,7 +479,7 @@ private void tagsView_KeyUp(object sender, KeyEventArgs e) tagsView.EndUpdate(); } } - + if (e.KeyCode == Keys.Delete && tagsView.SelectedItems.Count != 0) removeTagToolStripMenuItem_Click(null, null); @@ -488,7 +487,7 @@ private void tagsView_KeyUp(object sender, KeyEventArgs e) private void genericStrip_Opening(object sender, System.ComponentModel.CancelEventArgs e) { - var selectedTab = tabControl1.SelectedTab; + TabPage selectedTab = tabControl1.SelectedTab; var combinedPath = string.Empty; copyToolStripMenuItem1.Enabled = copyPathToolStripMenuItem.Enabled = openInFileExplorerToolStripMenuItem.Enabled = false; if (selectedTab == fileHierachyPage) @@ -514,44 +513,46 @@ private void genericStrip_Opening(object sender, System.ComponentModel.CancelEve if (erroredFilesList.SelectedItems.Count == 0) return; combinedPath = Path.Combine(extractionRecord.DestinationPath, erroredFilesList.SelectedItems[0].Text); copyToolStripMenuItem.Enabled = copyPathToolStripMenuItem.Enabled = true; - } else if (selectedTab == errorMessagesPage) + } + else if (selectedTab == errorMessagesPage) { if (errorMessagesList.SelectedItems.Count == 0) return; copyToolStripMenuItem.Enabled = true; } else return; - + openInFileExplorerToolStripMenuItem.Enabled = File.Exists(combinedPath) || Directory.Exists(combinedPath); } private void copyToolStripMenuItem1_Click(object sender, EventArgs e) { - var selectedTab = tabControl1.SelectedTab; + TabPage selectedTab = tabControl1.SelectedTab; if (selectedTab == fileHierachyPage) Clipboard.SetText(fileTreeView.SelectedNode.Text); else if (selectedTab == contentFoldersPage) { - List list = new List(contentFoldersList.SelectedItems.Count); + var list = new List(contentFoldersList.SelectedItems.Count); for (var i = 0; i < contentFoldersList.SelectedItems.Count; i++) list.Add(contentFoldersList.SelectedItems[i].Text); Clipboard.SetText(string.Join('\n', list)); } else if (selectedTab == fileListPage) { - List list = new List(filesExtractedList.SelectedItems.Count); + var list = new List(filesExtractedList.SelectedItems.Count); for (var i = 0; i < filesExtractedList.SelectedItems.Count; i++) list.Add(filesExtractedList.SelectedItems[i].Text); Clipboard.SetText(string.Join('\n', list)); } else if (selectedTab == erroredFilesPage) { - List list = new List(erroredFilesList.SelectedItems.Count); + var list = new List(erroredFilesList.SelectedItems.Count); for (var i = 0; i < erroredFilesList.SelectedItems.Count; i++) list.Add(erroredFilesList.SelectedItems[i].Text); Clipboard.SetText(string.Join('\n', list)); - } else if (selectedTab == errorMessagesPage) + } + else if (selectedTab == errorMessagesPage) { - List list = new List(errorMessagesList.SelectedItems.Count); + var list = new List(errorMessagesList.SelectedItems.Count); for (var i = 0; i < errorMessagesList.SelectedItems.Count; i++) list.Add(errorMessagesList.SelectedItems[i].Text); Clipboard.SetText(string.Join('\n', list)); @@ -560,26 +561,26 @@ private void copyToolStripMenuItem1_Click(object sender, EventArgs e) private void copyPathToolStripMenuItem_Click(object sender, EventArgs e) { - var selectedTab = tabControl1.SelectedTab; + TabPage selectedTab = tabControl1.SelectedTab; if (selectedTab == fileHierachyPage) Clipboard.SetText(Path.Combine(extractionRecord.DestinationPath, fileTreeView.SelectedNode.FullPath)); else if (selectedTab == contentFoldersPage) { - List list = new List(contentFoldersList.SelectedItems.Count); + var list = new List(contentFoldersList.SelectedItems.Count); for (var i = 0; i < contentFoldersList.SelectedItems.Count; i++) list.Add(Path.Combine(extractionRecord.DestinationPath, contentFoldersList.SelectedItems[i].Text)); Clipboard.SetText(string.Join('\n', list)); } else if (selectedTab == fileListPage) { - List list = new List(filesExtractedList.SelectedItems.Count); + var list = new List(filesExtractedList.SelectedItems.Count); for (var i = 0; i < filesExtractedList.SelectedItems.Count; i++) list.Add(Path.Combine(extractionRecord.DestinationPath, filesExtractedList.SelectedItems[i].Text)); Clipboard.SetText(string.Join('\n', list)); } else if (selectedTab == erroredFilesPage) { - List list = new List(erroredFilesList.SelectedItems.Count); + var list = new List(erroredFilesList.SelectedItems.Count); for (var i = 0; i < erroredFilesList.SelectedItems.Count; i++) list.Add(Path.Combine(extractionRecord.DestinationPath, erroredFilesList.SelectedItems[i].Text)); Clipboard.SetText(string.Join('\n', list)); @@ -588,9 +589,9 @@ private void copyPathToolStripMenuItem_Click(object sender, EventArgs e) private void openInFileExplorerToolStripMenuItem_Click(object _, EventArgs __) { - var selectedTab = tabControl1.SelectedTab; + TabPage selectedTab = tabControl1.SelectedTab; if (selectedTab == fileHierachyPage) - Process.Start(@"explorer.exe", $"/select, \"{Path.Combine(extractionRecord.DestinationPath, fileTreeView.SelectedNode.FullPath).Replace('/','\\')}\""); + Process.Start(@"explorer.exe", $"/select, \"{Path.Combine(extractionRecord.DestinationPath, fileTreeView.SelectedNode.FullPath).Replace('/', '\\')}\""); else if (selectedTab == contentFoldersPage) Process.Start(@"explorer.exe", $"/select, \"{Path.Combine(extractionRecord.DestinationPath, contentFoldersList.SelectedItems[0].Text).Replace('/', '\\')}\""); else if (selectedTab == fileListPage) @@ -609,7 +610,7 @@ private void copyImageToolStripMenuItem_Click(object sender, EventArgs e) catch (Exception ex) { MessageBox.Show($"Failed to copy image to clipboard. REASON: {ex}", "Copy image failed", MessageBoxButtons.OK, MessageBoxIcon.Error); - DPCommon.WriteToLog($"Failed to copy image to clipboard. REASON: {ex}"); + // DPCommon.WriteToLog($"Failed to copy image to clipboard. REASON: {ex}"); } } @@ -635,15 +636,12 @@ private void removeImageToolStripMenuItem_Click(object sender, EventArgs e) thumbnailBox.Image = Resources.NoImageFound; } - private void ProductRecordForm_FormClosed(object sender, FormClosedEventArgs e) - { - DPDatabase.ProductRecordModified -= OnProductRecordModified; - } + private void ProductRecordForm_FormClosed(object sender, FormClosedEventArgs e) => Program.Database.ProductRecordModified -= OnProductRecordModified; private void thumbnailStrip_Opening(object sender, System.ComponentModel.CancelEventArgs e) { - removeImageToolStripMenuItem.Enabled = copyImagePathToolStripMenuItem.Enabled = - copyImageToolStripMenuItem.Enabled = openInFileExplorerToolStripMenuItem.Enabled = + removeImageToolStripMenuItem.Enabled = copyImagePathToolStripMenuItem.Enabled = + copyImageToolStripMenuItem.Enabled = openInFileExplorerToolStripMenuItem.Enabled = !string.IsNullOrEmpty(thumbnailBox.ImageLocation); } } diff --git a/src/Forms/ProductRecordForm.resx b/src/DAZ_Installer.Windows/Forms/ProductRecordForm.resx similarity index 100% rename from src/Forms/ProductRecordForm.resx rename to src/DAZ_Installer.Windows/Forms/ProductRecordForm.resx diff --git a/src/Forms/TagsManager.Designer.cs b/src/DAZ_Installer.Windows/Forms/TagsManager.Designer.cs similarity index 99% rename from src/Forms/TagsManager.Designer.cs rename to src/DAZ_Installer.Windows/Forms/TagsManager.Designer.cs index ed90b5d..14f630e 100644 --- a/src/Forms/TagsManager.Designer.cs +++ b/src/DAZ_Installer.Windows/Forms/TagsManager.Designer.cs @@ -1,4 +1,4 @@ -namespace DAZ_Installer.Forms +namespace DAZ_Installer.Windows.Forms { partial class TagsManager { diff --git a/src/Forms/TagsManager.cs b/src/DAZ_Installer.Windows/Forms/TagsManager.cs similarity index 59% rename from src/Forms/TagsManager.cs rename to src/DAZ_Installer.Windows/Forms/TagsManager.cs index 59f6b15..14fd7c8 100644 --- a/src/Forms/TagsManager.cs +++ b/src/DAZ_Installer.Windows/Forms/TagsManager.cs @@ -1,34 +1,20 @@ using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; using System.Windows.Forms; -using static System.Windows.Forms.VisualStyles.VisualStyleElement; -namespace DAZ_Installer.Forms +namespace DAZ_Installer.Windows.Forms { public partial class TagsManager : Form { internal string[] tags; - public TagsManager() - { - InitializeComponent(); - } + public TagsManager() => InitializeComponent(); - public TagsManager(string[] tags) : this() - { - this.tags = tagsTxtBox.Lines = tags; - } + public TagsManager(string[] tags) : this() => this.tags = tagsTxtBox.Lines = tags; private void updateBtn_Click(object sender, EventArgs e) { - List tags = new List(tagsTxtBox.Text.Split('\n')); - int c = 0; + var tags = new List(tagsTxtBox.Text.Split('\n')); + var c = 0; for (var i = 0; i < tags.Count; i++) { var tag = tags[i]; @@ -36,9 +22,10 @@ private void updateBtn_Click(object sender, EventArgs e) { c++; tags[i] = tags[tags.Count - 1 - i]; - } else if (tag.Length > 70) + } + else if (tag.Length > 70) { - MessageBox.Show("Some lines are greater than 70 characters, please make sure each line is no more than 70 characters and try again.", + MessageBox.Show("Some lines are greater than 70 characters, please make sure each line is no more than 70 characters and try again.", "Tags too long", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } @@ -48,9 +35,6 @@ private void updateBtn_Click(object sender, EventArgs e) Close(); } - private void restoreBtn_Click(object sender, EventArgs e) - { - tagsTxtBox.Lines = tags; - } + private void restoreBtn_Click(object sender, EventArgs e) => tagsTxtBox.Lines = tags; } } diff --git a/src/Forms/TagsManager.resx b/src/DAZ_Installer.Windows/Forms/TagsManager.resx similarity index 100% rename from src/Forms/TagsManager.resx rename to src/DAZ_Installer.Windows/Forms/TagsManager.resx diff --git a/src/DAZ_Installer.Windows/Pages/Extract.Designer.cs b/src/DAZ_Installer.Windows/Pages/Extract.Designer.cs new file mode 100644 index 0000000..52aaf94 --- /dev/null +++ b/src/DAZ_Installer.Windows/Pages/Extract.Designer.cs @@ -0,0 +1,260 @@ + +namespace DAZ_Installer.Windows.Pages +{ + partial class Extract + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + var resources = new System.ComponentModel.ComponentResourceManager(typeof(Extract)); + tabControl1 = new System.Windows.Forms.TabControl(); + fileListPage = new System.Windows.Forms.TabPage(); + fileListView = new System.Windows.Forms.ListView(); + filePathColumn = new System.Windows.Forms.ColumnHeader(); + fileListContextStrip = new System.Windows.Forms.ContextMenuStrip(components); + inspectFileListMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + selectInHierachyToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + openInExplorerToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + noFilesSelectedToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + fileHierachyPage = new System.Windows.Forms.TabPage(); + fileHierachyTree = new System.Windows.Forms.TreeView(); + archiveFolderIcons = new System.Windows.Forms.ImageList(components); + queuePage = new System.Windows.Forms.TabPage(); + fileHierachyContextStrip = new System.Windows.Forms.ContextMenuStrip(components); + inspectToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + selectInFileListToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + openInExplorerToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); + progressCombo = new UI.ProgressCombo(); + tabControl1.SuspendLayout(); + fileListPage.SuspendLayout(); + fileListContextStrip.SuspendLayout(); + fileHierachyPage.SuspendLayout(); + fileHierachyContextStrip.SuspendLayout(); + SuspendLayout(); + // + // tabControl1 + // + tabControl1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + tabControl1.Controls.Add(fileListPage); + tabControl1.Controls.Add(fileHierachyPage); + tabControl1.Controls.Add(queuePage); + tabControl1.Location = new System.Drawing.Point(31, 222); + tabControl1.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); + tabControl1.Name = "tabControl1"; + tabControl1.SelectedIndex = 0; + tabControl1.Size = new System.Drawing.Size(491, 97); + tabControl1.TabIndex = 1; + // + // fileListPage + // + fileListPage.Controls.Add(fileListView); + fileListPage.Location = new System.Drawing.Point(4, 24); + fileListPage.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); + fileListPage.Name = "fileListPage"; + fileListPage.Padding = new System.Windows.Forms.Padding(4, 2, 4, 2); + fileListPage.Size = new System.Drawing.Size(483, 69); + fileListPage.TabIndex = 0; + fileListPage.Text = "File List"; + fileListPage.UseVisualStyleBackColor = true; + // + // fileListView + // + fileListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { filePathColumn }); + fileListView.ContextMenuStrip = fileListContextStrip; + fileListView.Dock = System.Windows.Forms.DockStyle.Fill; + fileListView.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None; + fileListView.Location = new System.Drawing.Point(4, 2); + fileListView.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); + fileListView.MultiSelect = false; + fileListView.Name = "fileListView"; + fileListView.Size = new System.Drawing.Size(475, 65); + fileListView.TabIndex = 0; + fileListView.UseCompatibleStateImageBehavior = false; + fileListView.View = System.Windows.Forms.View.Details; + // + // filePathColumn + // + filePathColumn.Text = "File Path"; + filePathColumn.Width = 530; + // + // fileListContextStrip + // + fileListContextStrip.DropShadowEnabled = false; + fileListContextStrip.ImageScalingSize = new System.Drawing.Size(20, 20); + fileListContextStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { inspectFileListMenuItem, selectInHierachyToolStripMenuItem, openInExplorerToolStripMenuItem, noFilesSelectedToolStripMenuItem }); + fileListContextStrip.Name = "contextMenuStrip1"; + fileListContextStrip.RenderMode = System.Windows.Forms.ToolStripRenderMode.Professional; + fileListContextStrip.ShowImageMargin = false; + fileListContextStrip.Size = new System.Drawing.Size(144, 92); + fileListContextStrip.Opening += fileListContextStrip_Opening; + // + // inspectFileListMenuItem + // + inspectFileListMenuItem.Name = "inspectFileListMenuItem"; + inspectFileListMenuItem.Size = new System.Drawing.Size(143, 22); + inspectFileListMenuItem.Text = "Inspect"; + inspectFileListMenuItem.Visible = false; + // + // selectInHierachyToolStripMenuItem + // + selectInHierachyToolStripMenuItem.Name = "selectInHierachyToolStripMenuItem"; + selectInHierachyToolStripMenuItem.Size = new System.Drawing.Size(143, 22); + selectInHierachyToolStripMenuItem.Text = "Select in Hierachy"; + selectInHierachyToolStripMenuItem.Visible = false; + selectInHierachyToolStripMenuItem.Click += selectInHierachyToolStripMenuItem_Click; + // + // openInExplorerToolStripMenuItem + // + openInExplorerToolStripMenuItem.Name = "openInExplorerToolStripMenuItem"; + openInExplorerToolStripMenuItem.Size = new System.Drawing.Size(143, 22); + openInExplorerToolStripMenuItem.Text = "Open in Explorer"; + openInExplorerToolStripMenuItem.Visible = false; + // + // noFilesSelectedToolStripMenuItem + // + noFilesSelectedToolStripMenuItem.Enabled = false; + noFilesSelectedToolStripMenuItem.Name = "noFilesSelectedToolStripMenuItem"; + noFilesSelectedToolStripMenuItem.Size = new System.Drawing.Size(143, 22); + noFilesSelectedToolStripMenuItem.Text = "No Files Selected"; + // + // fileHierachyPage + // + fileHierachyPage.Controls.Add(fileHierachyTree); + fileHierachyPage.Location = new System.Drawing.Point(4, 24); + fileHierachyPage.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); + fileHierachyPage.Name = "fileHierachyPage"; + fileHierachyPage.Padding = new System.Windows.Forms.Padding(4, 2, 4, 2); + fileHierachyPage.Size = new System.Drawing.Size(483, 69); + fileHierachyPage.TabIndex = 1; + fileHierachyPage.Text = "File Hierachy"; + fileHierachyPage.UseVisualStyleBackColor = true; + // + // fileHierachyTree + // + fileHierachyTree.Dock = System.Windows.Forms.DockStyle.Fill; + fileHierachyTree.Indent = 21; + fileHierachyTree.Location = new System.Drawing.Point(4, 2); + fileHierachyTree.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); + fileHierachyTree.Name = "fileHierachyTree"; + fileHierachyTree.Size = new System.Drawing.Size(475, 65); + fileHierachyTree.StateImageList = archiveFolderIcons; + fileHierachyTree.TabIndex = 0; + // + // archiveFolderIcons + // + archiveFolderIcons.ColorDepth = System.Windows.Forms.ColorDepth.Depth8Bit; + archiveFolderIcons.ImageStream = (System.Windows.Forms.ImageListStreamer)resources.GetObject("archiveFolderIcons.ImageStream"); + archiveFolderIcons.TransparentColor = System.Drawing.SystemColors.Window; + archiveFolderIcons.Images.SetKeyName(0, "FolderIcon.png"); + archiveFolderIcons.Images.SetKeyName(1, "RARIcon.png"); + archiveFolderIcons.Images.SetKeyName(2, "ZIPIcon.png"); + // + // queuePage + // + queuePage.Location = new System.Drawing.Point(4, 24); + queuePage.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); + queuePage.Name = "queuePage"; + queuePage.Padding = new System.Windows.Forms.Padding(4, 2, 4, 2); + queuePage.Size = new System.Drawing.Size(483, 69); + queuePage.TabIndex = 2; + queuePage.Text = "Queue"; + queuePage.UseVisualStyleBackColor = true; + // + // fileHierachyContextStrip + // + fileHierachyContextStrip.ImageScalingSize = new System.Drawing.Size(20, 20); + fileHierachyContextStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { inspectToolStripMenuItem, selectInFileListToolStripMenuItem, openInExplorerToolStripMenuItem1 }); + fileHierachyContextStrip.Name = "fileHierachyContextStrip"; + fileHierachyContextStrip.Size = new System.Drawing.Size(163, 70); + // + // inspectToolStripMenuItem + // + inspectToolStripMenuItem.Name = "inspectToolStripMenuItem"; + inspectToolStripMenuItem.Size = new System.Drawing.Size(162, 22); + inspectToolStripMenuItem.Text = "Inspect"; + // + // selectInFileListToolStripMenuItem + // + selectInFileListToolStripMenuItem.Name = "selectInFileListToolStripMenuItem"; + selectInFileListToolStripMenuItem.Size = new System.Drawing.Size(162, 22); + selectInFileListToolStripMenuItem.Text = "Select in File List"; + selectInFileListToolStripMenuItem.Click += selectInFileListToolStripMenuItem_Click; + // + // openInExplorerToolStripMenuItem1 + // + openInExplorerToolStripMenuItem1.Name = "openInExplorerToolStripMenuItem1"; + openInExplorerToolStripMenuItem1.Size = new System.Drawing.Size(162, 22); + openInExplorerToolStripMenuItem1.Text = "Open in Explorer"; + // + // progressCombo + // + progressCombo.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + progressCombo.Location = new System.Drawing.Point(29, 22); + progressCombo.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + progressCombo.Name = "progressCombo"; + progressCombo.Size = new System.Drawing.Size(493, 196); + progressCombo.TabIndex = 3; + // + // Extract + // + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Inherit; + BackColor = System.Drawing.Color.White; + Controls.Add(tabControl1); + Controls.Add(progressCombo); + Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); + Name = "Extract"; + Size = new System.Drawing.Size(542, 344); + tabControl1.ResumeLayout(false); + fileListPage.ResumeLayout(false); + fileListContextStrip.ResumeLayout(false); + fileHierachyPage.ResumeLayout(false); + fileHierachyContextStrip.ResumeLayout(false); + ResumeLayout(false); + } + + #endregion + private System.Windows.Forms.TabControl tabControl1; + private System.Windows.Forms.TabPage fileListPage; + private System.Windows.Forms.TabPage fileHierachyPage; + private System.Windows.Forms.ListView fileListView; + private System.Windows.Forms.TreeView fileHierachyTree; + private System.Windows.Forms.ColumnHeader filePathColumn; + private System.Windows.Forms.ContextMenuStrip fileListContextStrip; + private System.Windows.Forms.ToolStripMenuItem inspectFileListMenuItem; + private System.Windows.Forms.ToolStripMenuItem selectInHierachyToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem openInExplorerToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem noFilesSelectedToolStripMenuItem; + private System.Windows.Forms.ContextMenuStrip fileHierachyContextStrip; + private System.Windows.Forms.ToolStripMenuItem inspectToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem selectInFileListToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem openInExplorerToolStripMenuItem1; + private System.Windows.Forms.TabPage queuePage; + private Custom_Controls.QueueControl queueControl1; + internal System.Windows.Forms.ImageList archiveFolderIcons; + internal UI.ProgressCombo progressCombo; + } +} diff --git a/src/DAZ_Installer.Windows/Pages/Extract.cs b/src/DAZ_Installer.Windows/Pages/Extract.cs new file mode 100644 index 0000000..e08288a --- /dev/null +++ b/src/DAZ_Installer.Windows/Pages/Extract.cs @@ -0,0 +1,201 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +using DAZ_Installer.Core; +using DAZ_Installer.Windows.DP; +using Serilog; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Windows.Forms; + +namespace DAZ_Installer.Windows.Pages +{ + + public partial class Extract : UserControl + { + public static Extract ExtractPage = null!; + internal static Dictionary associatedListItems = new(2048); + internal static Dictionary associatedTreeNodes = new(2048); + + public Extract() + { + InitializeComponent(); + ExtractPage = this; + tabControl1.TabPages.Remove(queuePage); + queuePage.Dispose(); + } + + /// + /// Adds all the contents found in to the list view. + /// Assure that this function is called from the UI thread with either or . + /// + internal void AddToList(DPArchive archive) + { + fileListView.BeginUpdate(); + foreach (DPFile content in archive.Contents.Values) + { + ListViewItem item = fileListView.Items.Add($"{archive.FileName}\\{content.Path}"); + item.Tag = content; + associatedListItems[content] = item; + } + fileListView.Columns[0].AutoResize(ColumnHeaderAutoResizeStyle.ColumnContent); + fileListView.EndUpdate(); + } + + /// + /// Process the child nodes of and add them to . + /// Assure that this function is called from the UI thread with either or . + /// + /// + /// + private void ProcessChildNodes(DPFolder folder, TreeNode parentNode) + { + var fileName = Path.GetFileName(folder.Path); + // We don't need associations for folders. + var folder1 = parentNode.Nodes.Add(fileName); + AddIcon(folder1, null); + + // Add the DPFiles. + foreach (DPFile file in folder.Contents) + { + fileName = Path.GetFileName(file.Path); + // TO DO: Add condition if file is a DPArchive & extract == true + TreeNode node = folder1.Nodes.Add(fileName); + node.Tag = file; + associatedTreeNodes[file] = node; + AddIcon(node, file.Ext); + } + foreach (DPFolder subfolder in folder.subfolders) + ProcessChildNodes(subfolder, folder1); + } + + /// + /// Adds the contents of to the file hierachy tree. + /// Assure that this function is called from the UI thread with either or . + /// + /// The archive to add to the hierachy + internal void AddToHierachy(DPArchive workingArchive) + { + fileHierachyTree.BeginUpdate(); + + // Add root node for DPArchive. + var fileName = workingArchive.FileName; + TreeNode rootNode = fileHierachyTree.Nodes.Add(fileName); + rootNode.Tag = workingArchive; + associatedTreeNodes[workingArchive] = rootNode; + AddIcon(rootNode, workingArchive.Ext); + + // Add any files that aren't in any folder. + foreach (DPFile file in workingArchive.RootContents) + { + fileName = Path.GetFileName(file.Path); + TreeNode node = rootNode.Nodes.Add(fileName); + node.Tag = file; + associatedTreeNodes[file] = node; + AddIcon(node, file.Ext); + } + + // Recursively add files & folder within each folder. + foreach (DPFolder folder in workingArchive.RootFolders) + ProcessChildNodes(folder, rootNode); + + fileHierachyTree.EndUpdate(); + } + + /// + /// Assigns an icon to the based on the of the file. + /// This only assigns icons for archives and folders.
+ /// Assure that this function is called from the UI thread with either or . + ///
+ /// The node to set the icon to + /// The extension used to determine the icon to set (7z, zip, rar, null, or ""). + private void AddIcon(TreeNode node, string? ext) + { + if (string.IsNullOrEmpty(ext)) + node.StateImageIndex = 0; + else if (ext.Contains("zip") || ext.Contains("7z")) + node.StateImageIndex = 2; + else if (ext.Contains("rar")) + node.StateImageIndex = 1; + } + + public void ResetExtractPage() + { + // Later show nothing to extract panel. + progressCombo.EndProgress(); + fileListView.Items.Clear(); + fileHierachyTree.Nodes.Clear(); + associatedListItems.Clear(); + associatedTreeNodes.Clear(); + } + + /// + /// Recursively gets the controls of the and returns them as an array. + /// + /// + /// + public static IEnumerable RecursivelyGetControls(Control obj) + { + Log.Information($"RecursivelyGetControls: {obj.Controls.Count}"); + if (obj.Controls.Count == 0) return Enumerable.Empty(); + var list = new List(obj.Controls.Count); + var enumerator = list.AsEnumerable(); + foreach (Control control in obj.Controls) + { + enumerator.Concat(RecursivelyGetControls(control)); + list.Add(control); + } + + return enumerator; + } + + private void mainProcLbl_Click(object sender, EventArgs e) + { + + } + + #region Context Strip Events + private void selectInHierachyToolStripMenuItem_Click(object sender, EventArgs e) + { + // Get the associated file with listviewitem. + var file = fileListView.SelectedItems[0].Tag as DPAbstractNode; + + if (file != null && associatedTreeNodes.TryGetValue(file, out TreeNode node)) + fileHierachyTree.SelectedNode = node; + // Switch tab. + tabControl1.SelectTab(fileHierachyPage); + } + + private void fileListContextStrip_Opening(object sender, CancelEventArgs e) + { + var filesSelected = fileListView.SelectedItems.Count != 0; + inspectFileListMenuItem.Visible = false && filesSelected; + openInExplorerToolStripMenuItem.Visible = filesSelected; + selectInHierachyToolStripMenuItem.Visible = filesSelected && + associatedTreeNodes.TryGetValue(fileListView.SelectedItems[0].Tag as DPAbstractNode, out TreeNode _); + noFilesSelectedToolStripMenuItem.Visible = !filesSelected; + } + + public void OpenFileInExplorer(string path) => Process.Start(@"explorer.exe", $"/select, \"{path}\""); + #endregion + + private void selectInFileListToolStripMenuItem_Click(object sender, EventArgs e) + { + // Get the associated file with listviewitem. + var file = fileHierachyTree.SelectedNode.Tag as DPAbstractNode; + + if (file != null && associatedListItems.TryGetValue(file, out ListViewItem node)) + node.Selected = true; + + // Switch tab. + tabControl1.SelectTab(fileListPage); + } + } + +} + + diff --git a/src/Custom Controls/Extract.resx b/src/DAZ_Installer.Windows/Pages/Extract.resx similarity index 71% rename from src/Custom Controls/Extract.resx rename to src/DAZ_Installer.Windows/Pages/Extract.resx index 1b36b92..0abce93 100644 --- a/src/Custom Controls/Extract.resx +++ b/src/DAZ_Installer.Windows/Pages/Extract.resx @@ -1,4 +1,64 @@ - + + + @@ -68,7 +128,7 @@ AAEAAAD/////AQAAAAAAAAAMAgAAAEZTeXN0ZW0uV2luZG93cy5Gb3JtcywgQ3VsdHVyZT1uZXV0cmFs LCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAAAmU3lzdGVtLldpbmRvd3MuRm9ybXMu SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAAlgkAAAJNU0Z0AUkBTAIBAQMB - AAFwAQABcAEAARABAAEQAQAE/wEJAQAI/wFCAU0BNgEEBgABNgEEAgABKAMAAUADAAEQAwABAQEAAQgG + AAGIAQABiAEAARABAAEQAQAE/wEJAQAI/wFCAU0BNgEEBgABNgEEAgABKAMAAUADAAEQAwABAQEAAQgG AAEEGAABgAIAAYADAAKAAQABgAMAAYABAAGAAQACgAIAA8ABAAHAAdwBwAEAAfABygGmAQABMwUAATMB AAEzAQABMwEAAjMCAAMWAQADHAEAAyIBAAMpAQADVQEAA00BAANCAQADOQEAAYABfAH/AQACUAH/AQAB kwEAAdYBAAH/AewBzAEAAcYB1gHvAQAB1gLnAQABkAGpAa0CAAH/ATMDAAFmAwABmQMAAcwCAAEzAwAC diff --git a/src/DAZ_Installer.Windows/Pages/Home.Designer.cs b/src/DAZ_Installer.Windows/Pages/Home.Designer.cs new file mode 100644 index 0000000..20a9caf --- /dev/null +++ b/src/DAZ_Installer.Windows/Pages/Home.Designer.cs @@ -0,0 +1,220 @@ + +namespace DAZ_Installer.Windows.Pages +{ + partial class Home + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + titleLbl = new System.Windows.Forms.Label(); + extractBtn = new System.Windows.Forms.Button(); + addMoreFilesBtn = new System.Windows.Forms.Button(); + clearListBtn = new System.Windows.Forms.Button(); + listView1 = new System.Windows.Forms.ListView(); + columnHeader1 = new System.Windows.Forms.ColumnHeader(); + homeListContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(components); + removeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + addMoreItemsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + openFileDialog1 = new System.Windows.Forms.OpenFileDialog(); + tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + dropBtn = new System.Windows.Forms.Button(); + homeListContextMenuStrip.SuspendLayout(); + tableLayoutPanel1.SuspendLayout(); + SuspendLayout(); + // + // titleLbl + // + titleLbl.BackColor = System.Drawing.Color.White; + titleLbl.Dock = System.Windows.Forms.DockStyle.Top; + titleLbl.Font = new System.Drawing.Font("Segoe UI Variable Text Light", 18F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + titleLbl.ForeColor = System.Drawing.Color.FromArgb(31, 31, 31); + titleLbl.Location = new System.Drawing.Point(0, 0); + titleLbl.Name = "titleLbl"; + titleLbl.Size = new System.Drawing.Size(542, 55); + titleLbl.TabIndex = 0; + titleLbl.Text = "Product Manager for DAZ Studio"; + titleLbl.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // extractBtn + // + extractBtn.Dock = System.Windows.Forms.DockStyle.Fill; + extractBtn.Location = new System.Drawing.Point(130, 208); + extractBtn.Name = "extractBtn"; + extractBtn.Size = new System.Drawing.Size(248, 38); + extractBtn.TabIndex = 2; + extractBtn.Text = "Extract File(s)"; + extractBtn.UseVisualStyleBackColor = true; + extractBtn.Click += button1_Click; + // + // addMoreFilesBtn + // + addMoreFilesBtn.Dock = System.Windows.Forms.DockStyle.Fill; + addMoreFilesBtn.Location = new System.Drawing.Point(3, 208); + addMoreFilesBtn.Name = "addMoreFilesBtn"; + addMoreFilesBtn.Size = new System.Drawing.Size(121, 38); + addMoreFilesBtn.TabIndex = 4; + addMoreFilesBtn.Text = "Add more files"; + addMoreFilesBtn.UseVisualStyleBackColor = true; + addMoreFilesBtn.Click += addMoreFilesBtn_Click; + // + // clearListBtn + // + clearListBtn.Dock = System.Windows.Forms.DockStyle.Fill; + clearListBtn.Location = new System.Drawing.Point(384, 208); + clearListBtn.Name = "clearListBtn"; + clearListBtn.Size = new System.Drawing.Size(122, 38); + clearListBtn.TabIndex = 3; + clearListBtn.Text = "Clear List"; + clearListBtn.UseVisualStyleBackColor = true; + clearListBtn.Click += clearListBtn_Click; + // + // listView1 + // + listView1.AllowDrop = true; + listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { columnHeader1 }); + tableLayoutPanel1.SetColumnSpan(listView1, 3); + listView1.ContextMenuStrip = homeListContextMenuStrip; + listView1.Dock = System.Windows.Forms.DockStyle.Fill; + listView1.ForeColor = System.Drawing.SystemColors.WindowText; + listView1.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None; + listView1.Location = new System.Drawing.Point(3, 3); + listView1.Name = "listView1"; + listView1.Size = new System.Drawing.Size(503, 199); + listView1.TabIndex = 4; + listView1.UseCompatibleStateImageBehavior = false; + listView1.View = System.Windows.Forms.View.Details; + // + // columnHeader1 + // + columnHeader1.Text = "a"; + columnHeader1.Width = 550; + // + // homeListContextMenuStrip + // + homeListContextMenuStrip.DropShadowEnabled = false; + homeListContextMenuStrip.ImageScalingSize = new System.Drawing.Size(20, 20); + homeListContextMenuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { removeToolStripMenuItem, addMoreItemsToolStripMenuItem }); + homeListContextMenuStrip.Name = "homeListContextMenuStrip"; + homeListContextMenuStrip.ShowImageMargin = false; + homeListContextMenuStrip.Size = new System.Drawing.Size(144, 48); + homeListContextMenuStrip.Opening += homeListContextMenuStrip_Opening; + // + // removeToolStripMenuItem + // + removeToolStripMenuItem.Name = "removeToolStripMenuItem"; + removeToolStripMenuItem.Size = new System.Drawing.Size(143, 22); + removeToolStripMenuItem.Text = "Remove"; + removeToolStripMenuItem.Click += removeToolStripMenuItem_Click; + // + // addMoreItemsToolStripMenuItem + // + addMoreItemsToolStripMenuItem.Name = "addMoreItemsToolStripMenuItem"; + addMoreItemsToolStripMenuItem.Size = new System.Drawing.Size(143, 22); + addMoreItemsToolStripMenuItem.Text = "Add more items..."; + addMoreItemsToolStripMenuItem.Click += addMoreItemsToolStripMenuItem_Click; + // + // openFileDialog1 + // + openFileDialog1.AddExtension = false; + openFileDialog1.DefaultExt = "zip"; + openFileDialog1.Filter = "RAR files (*.rar)|*.rar|ZIP files (*.zip)|*.zip|7z files (*.7z)|*.7z|7z part file base(*.001)|*.001|All files (*.*)|*.*"; + openFileDialog1.Multiselect = true; + openFileDialog1.SupportMultiDottedExtensions = true; + // + // tableLayoutPanel1 + // + tableLayoutPanel1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + tableLayoutPanel1.ColumnCount = 3; + tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 25F)); + tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); + tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 25F)); + tableLayoutPanel1.Controls.Add(extractBtn, 1, 1); + tableLayoutPanel1.Controls.Add(clearListBtn, 2, 1); + tableLayoutPanel1.Controls.Add(addMoreFilesBtn, 0, 1); + tableLayoutPanel1.Controls.Add(listView1, 0, 0); + tableLayoutPanel1.Location = new System.Drawing.Point(18, 55); + tableLayoutPanel1.Name = "tableLayoutPanel1"; + tableLayoutPanel1.RowCount = 2; + tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 44F)); + tableLayoutPanel1.Size = new System.Drawing.Size(509, 249); + tableLayoutPanel1.TabIndex = 5; + // + // dropBtn + // + dropBtn.AllowDrop = true; + dropBtn.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + dropBtn.BackColor = System.Drawing.Color.FromArgb(192, 255, 192); + dropBtn.Cursor = System.Windows.Forms.Cursors.Hand; + dropBtn.FlatAppearance.BorderSize = 0; + dropBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + dropBtn.Font = new System.Drawing.Font("Segoe UI Variable Display Semil", 18F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + dropBtn.ForeColor = System.Drawing.Color.FromArgb(32, 32, 32); + dropBtn.Location = new System.Drawing.Point(18, 55); + dropBtn.Name = "dropBtn"; + dropBtn.Size = new System.Drawing.Size(509, 249); + dropBtn.TabIndex = 6; + dropBtn.Text = "Click here to select file(s) or drag them here."; + dropBtn.UseVisualStyleBackColor = false; + dropBtn.Click += dropBtn_Click; + dropBtn.DragEnter += dropBtn_DragEnter; + dropBtn.DragLeave += dropBtn_DragLeave; + // + // Home + // + AllowDrop = true; + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Inherit; + AutoSize = true; + BackColor = System.Drawing.Color.White; + Controls.Add(dropBtn); + Controls.Add(titleLbl); + Controls.Add(tableLayoutPanel1); + Name = "Home"; + Size = new System.Drawing.Size(542, 344); + DragDrop += Home_DragDrop; + DragEnter += Home_DragEnter; + homeListContextMenuStrip.ResumeLayout(false); + tableLayoutPanel1.ResumeLayout(false); + ResumeLayout(false); + } + + #endregion + + private System.Windows.Forms.Label titleLbl; + private System.Windows.Forms.Button extractBtn; + private System.Windows.Forms.OpenFileDialog openFileDialog1; + internal System.Windows.Forms.ListView listView1; + private System.Windows.Forms.ContextMenuStrip homeListContextMenuStrip; + private System.Windows.Forms.ToolStripMenuItem removeToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem addMoreItemsToolStripMenuItem; + private System.Windows.Forms.Button addMoreFilesBtn; + private System.Windows.Forms.Button clearListBtn; + private System.Windows.Forms.ColumnHeader columnHeader1; + private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; + private System.Windows.Forms.Button dropBtn; + } +} diff --git a/src/Custom Controls/Home.cs b/src/DAZ_Installer.Windows/Pages/Home.cs similarity index 63% rename from src/Custom Controls/Home.cs rename to src/DAZ_Installer.Windows/Pages/Home.cs index 93f8e14..e74ff8f 100644 --- a/src/Custom Controls/Home.cs +++ b/src/DAZ_Installer.Windows/Pages/Home.cs @@ -1,99 +1,72 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE +using DAZ_Installer.Core; +using DAZ_Installer.Windows.Forms; +using DAZ_Installer.Windows.DP; using System; -using System.IO; using System.Collections.Generic; using System.ComponentModel; -using System.Drawing; -using System.Linq; +using System.IO; using System.Text; -using System.Threading; -using System.Threading.Tasks; using System.Windows.Forms; -using DAZ_Installer.DP; -namespace DAZ_Installer +namespace DAZ_Installer.Windows.Pages { public partial class Home : UserControl { - public static Home HomePage; + public static Home HomePage = null!; public Home() { InitializeComponent(); HomePage = this; + titleLbl.Text = Program.AppName; + + RegisterGlobalEvents(this); } - private void dropBtn_Click(object sender, EventArgs e) + private void RegisterGlobalEvents(Control parent) { - HandleOpenDialogue(); + foreach (Control control in parent.Controls) + { + control.AllowDrop = true; + control.DragDrop += Home_DragDrop; + control.DragEnter += Home_DragEnter; + RegisterGlobalEvents(control); + } } + private void dropBtn_Click(object sender, EventArgs e) => HandleOpenDialogue(); + internal void button1_Click(object sender, EventArgs e) { - var eControl = Extract.ExtractPage; + // Check that the settings are valid first. If not, do not proceed. + if (!DPSettings.CurrentSettingsObject.Valid) + { + MessageBox.Show("The current settings are not valid for processing. This could be due a directory not existing, the application does not have authorized access to access the directory, or due to" + + "an unknown IO issue. Please check your settings.", "Settings invalid", + MessageBoxButtons.OK, MessageBoxIcon.Warning); + MainForm.SwitchPage(Settings.settingsPage); + return; + } + // Clear everything from extract page. - DPProgressCombo.RemoveAll(); + Extract.ExtractPage.ResetExtractPage(); + // Goto next page. - MainForm.SwitchPage(eControl); + MainForm.SwitchPage(Extract.ExtractPage); + var newJob = new DPExtractJob(listView1.Items); // Todo: make a list. - var newTask = new Task(newJob.DoJob); + newJob.DoJob(); - newTask.Start(); // Clear list and reset home. clearListBtn_Click(null, null); } - private void dropBtn_DragEnter(object sender, DragEventArgs e) - { - dropBtn.Text = "Drop here!"; - e.Effect = DPCommon.dropEffect; - } + private void dropBtn_DragEnter(object sender, DragEventArgs e) => dropBtn.Text = "Drop here!"; - private void dropBtn_DragLeave(object sender, EventArgs e) - { - dropBtn.Text = "Click here to select file(s) or drag them here."; - } - - private void dropBtn_DragDrop(object sender, DragEventArgs e) - { - string[] tmp = (string[]) e.Data.GetData(DataFormats.FileDrop, false); - Queue invalidFiles = new(); - listView1.BeginUpdate(); - // Check for string if it's valid. - foreach (var path in tmp) - { - var fileInfo = new FileInfo(path); - var ext = fileInfo.Extension; - ext = ext.IndexOf('.') != -1 ? ext.Substring(1) : ext; - if (fileInfo.Exists && DPFile.ValidImportExtension(ext)) - { - // Add to list. - listView1.Items.Add(path); - } else - { - var type = DPAbstractArchive.DetermineArchiveFormatPrecise(path); - if (type == ArchiveFormat.SevenZ && ext.EndsWith("001")) - listView1.Items.Add(path); - else invalidFiles.Enqueue(path); - } - } - listView1.EndUpdate(); - if (invalidFiles.Count > 0) - { - var builder = new StringBuilder(50); - while (invalidFiles.Count != 0) - builder.AppendLine(" \u2022 " + invalidFiles.Dequeue()); - MessageBox.Show("Files that cannot be processed where removed from the list." + - "\nRemoved files:\n" + builder.ToString(), "Invalid files removed", MessageBoxButtons.OK, - MessageBoxIcon.Warning); - } - if (listView1.Items.Count != 0 ) - dropBtn.Visible = dropBtn.Enabled = false; - - dropBtn.Text = "Click here to select file(s) or drag them here."; - } + private void dropBtn_DragLeave(object sender, EventArgs e) => dropBtn.Text = "Click here to select file(s) or drag them here."; private void removeToolStripMenuItem_Click(object sender, EventArgs e) { @@ -103,11 +76,9 @@ private void removeToolStripMenuItem_Click(object sender, EventArgs e) } if (listView1.Items.Count == 0) controlDragPanel(true); } - private void addMoreFilesBtn_Click(object sender, EventArgs e) - { + private void addMoreFilesBtn_Click(object sender, EventArgs e) => // Show dialogue. HandleOpenDialogue(); - } private void controlDragPanel(bool visible) { @@ -117,7 +88,7 @@ private void controlDragPanel(bool visible) private void HandleOpenDialogue() { - var result = openFileDialog1.ShowDialog(); + DialogResult result = openFileDialog1.ShowDialog(); if (result == DialogResult.OK) { listView1.BeginUpdate(); @@ -143,14 +114,49 @@ private void homeListContextMenuStrip_Opening(object sender, CancelEventArgs e) removeToolStripMenuItem.Visible = hasSelectedItems; } - private void addMoreItemsToolStripMenuItem_Click(object sender, EventArgs e) - { - HandleOpenDialogue(); - } + private void addMoreItemsToolStripMenuItem_Click(object sender, EventArgs e) => HandleOpenDialogue(); - private void listView1_DragEnter(object sender, DragEventArgs e) + private void Home_DragEnter(object sender, DragEventArgs e) => e.Effect = Program.DropEffect; + + private void Home_DragDrop(object sender, DragEventArgs e) { - e.Effect = DPCommon.dropEffect; + if (e.Data is null) return; + var tmp = (string[])e.Data.GetData(DataFormats.FileDrop, false); + Queue invalidFiles = new(); + listView1.BeginUpdate(); + // Check for string if it's valid. + foreach (var path in tmp) + { + var fileInfo = new FileInfo(path); + var ext = fileInfo.Extension; + ext = ext.IndexOf('.') != -1 ? ext.Substring(1) : ext; + if (fileInfo.Exists && DPFile.ValidImportExtension(ext)) + { + // Add to list. + listView1.Items.Add(path); + } + else + { + ArchiveFormat type = DPArchive.DetermineArchiveFormatPrecise(path); // TODO: I'm pretty sure this can be removed. + if (type == ArchiveFormat.SevenZ && ext.EndsWith("001")) + listView1.Items.Add(path); + else invalidFiles.Enqueue(path); + } + } + listView1.EndUpdate(); + if (invalidFiles.Count > 0) + { + var builder = new StringBuilder(50); + while (invalidFiles.Count != 0) + builder.AppendLine(" \u2022 " + invalidFiles.Dequeue()); + MessageBox.Show("Files that cannot be processed where removed from the list." + + "\nRemoved files:\n" + builder.ToString(), "Invalid files removed", MessageBoxButtons.OK, + MessageBoxIcon.Warning); + } + if (listView1.Items.Count != 0) + dropBtn.Visible = dropBtn.Enabled = false; + + dropBtn.Text = "Click here to select file(s) or drag them here."; } } } diff --git a/src/Custom Controls/Home.resx b/src/DAZ_Installer.Windows/Pages/Home.resx similarity index 52% rename from src/Custom Controls/Home.resx rename to src/DAZ_Installer.Windows/Pages/Home.resx index fe944c3..2b48a4f 100644 --- a/src/Custom Controls/Home.resx +++ b/src/DAZ_Installer.Windows/Pages/Home.resx @@ -1,4 +1,64 @@ - + + + diff --git a/src/Custom Controls/Library.Designer.cs b/src/DAZ_Installer.Windows/Pages/Library.Designer.cs similarity index 99% rename from src/Custom Controls/Library.Designer.cs rename to src/DAZ_Installer.Windows/Pages/Library.Designer.cs index 288bd7b..c2f7daf 100644 --- a/src/Custom Controls/Library.Designer.cs +++ b/src/DAZ_Installer.Windows/Pages/Library.Designer.cs @@ -1,5 +1,5 @@  -namespace DAZ_Installer +namespace DAZ_Installer.Windows.Pages { partial class Library { diff --git a/src/Custom Controls/Library.cs b/src/DAZ_Installer.Windows/Pages/Library.cs similarity index 84% rename from src/Custom Controls/Library.cs rename to src/DAZ_Installer.Windows/Pages/Library.cs index 1954399..704e5b1 100644 --- a/src/Custom Controls/Library.cs +++ b/src/DAZ_Installer.Windows/Pages/Library.cs @@ -1,15 +1,16 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE +using DAZ_Installer.Database; +using DAZ_Installer.Windows.Forms; using System; -using System.IO; +using System.Collections.Generic; using System.Drawing; -using System.Threading; +using System.IO; using System.Threading.Tasks; using System.Windows.Forms; -using System.Collections.Generic; -using DAZ_Installer.DP; +using DAZ_Installer.Windows.DP; -namespace DAZ_Installer +namespace DAZ_Installer.Windows.Pages { /// /// The Library class is responsible for the loading, adding & removing LibraryItems. It is also responsible for controlling the LibraryPanel and effectively managing image resources. It also controls search interactions. @@ -22,7 +23,7 @@ public partial class Library : UserControl protected const byte maxImagesLoad = byte.MaxValue; protected const byte maxListSize = 25; protected byte maxImageFit; - protected List libraryItems { get => libraryPanel1.LibraryItems; } + protected List libraryItems => libraryPanel1.LibraryItems; protected List searchItems { get => libraryPanel1.SearchItems; set => libraryPanel1.SearchItems = value; } protected DPProductRecord[] ProductRecords { get; set; } = new DPProductRecord[0]; private DPProductRecord[] SearchRecords { get; set; } = new DPProductRecord[0]; @@ -55,29 +56,29 @@ private void Library_Load(object sender, EventArgs e) libraryPanel1.CurrentPage = 1; Task.Run(LoadLibraryItems); libraryPanel1.AddPageChangeListener(UpdatePage); - DPDatabase.ProductRecordAdded += OnAddedProductRecord; - DPDatabase.ProductRecordRemoved += OnRemovedProductRecord; - DPDatabase.ProductRecordModified += OnModifiedProductRecord; + Program.Database.ProductRecordAdded += OnAddedProductRecord; + Program.Database.ProductRecordRemoved += OnRemovedProductRecord; + Program.Database.ProductRecordModified += OnModifiedProductRecord; } // Called on a different thread. private void LoadLibraryItemImages() { thumbnails.Images.Clear(); - thumbnails.Images.Add(Properties.Resources.NoImageFound); + thumbnails.Images.Add(Resources.NoImageFound); noImageFound = thumbnails.Images[0]; - + mainImagesLoaded = true; - DPCommon.WriteToLog("Loaded images."); + // DPCommon.WriteToLog("Loaded images."); } private void LoadLibraryItems() { if (Program.IsRunByIDE && !IsHandleCreated) return; - DPDatabase.GetProductRecordsQ(SortMethod, (uint)libraryPanel1.CurrentPage, 25, 0, OnLibraryQueryUpdate); + Program.Database.GetProductRecordsQ(SortMethod, libraryPanel1.CurrentPage, 25, 0, OnLibraryQueryUpdate); // Invoke or BeginInvoke cannot be called on a control until the window handle has been created.' - DPCommon.WriteToLog("Loaded library items."); + // DPCommon.WriteToLog("Loaded library items."); } private void SetupSortMethodCombo() @@ -97,7 +98,7 @@ private void ClearPageContents() libraryPanel1.EditMode = true; if (searchMode) { - foreach (var lb in libraryPanel1.LibraryItems) + foreach (LibraryItem lb in libraryPanel1.LibraryItems) { if (lb == null || lb.ProductRecord == null) continue; @@ -114,7 +115,7 @@ private void ClearPageContents() libraryPanel1.EditMode = false; return; } - foreach (var lb in libraryPanel1.SearchItems) + foreach (LibraryItem lb in libraryPanel1.SearchItems) { if (lb == null || lb.ProductRecord == null) continue; @@ -134,6 +135,7 @@ internal LibraryItem AddNewSearchItem(DPProductRecord record) var searchItem = new LibraryItem(); searchItem.TitleText = record.ProductName; + searchItem.MaxTagCount = DPSettings.CurrentSettingsObject.MaxTagsToShow; searchItem.Tags = record.Tags; searchItem.Dock = DockStyle.Top; searchItem.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top; @@ -149,7 +151,10 @@ internal LibraryItem AddNewLibraryItem(DPProductRecord record) return (LibraryItem)Invoke(new Func(AddNewLibraryItem), record); } var lb = new LibraryItem(); + lb.Database = Program.Database; + lb.ProductRecordFormType = typeof(ProductRecordForm); lb.TitleText = record.ProductName; + lb.MaxTagCount = DPSettings.CurrentSettingsObject.MaxTagsToShow; lb.Tags = record.Tags; lb.Dock = DockStyle.Top; lb.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top; @@ -245,7 +250,7 @@ internal void TryPageUpdate() public void ForcePageUpdate() { if (InvokeRequired) { Invoke(ForcePageUpdate); return; } - DPCommon.WriteToLog("force page update called."); + // DPCommon.WriteToLog("force page update called."); ClearPageContents(); ClearSearchItems(); ClearLibraryItems(); @@ -261,12 +266,12 @@ public void ForcePageUpdate() // TODO: Potential previous page == the same dispite mode. public void UpdatePage(uint page) { - DPCommon.WriteToLog("page update called."); + // DPCommon.WriteToLog("page update called."); // if (page == libraryPanel1.PreviousPage) return; if (!searchMode) { - DPDatabase.GetProductRecordsQ(SortMethod, page, 25, callback: OnLibraryQueryUpdate); + Program.Database.GetProductRecordsQ(SortMethod, page, 25, callback: OnLibraryQueryUpdate); } else { @@ -276,22 +281,22 @@ public void UpdatePage(uint page) private void UpdatePageCount() { - uint pageCount = searchMode ? + var pageCount = searchMode ? (uint)Math.Ceiling(SearchRecords.Length / 25f) : - (uint)Math.Ceiling(DPDatabase.ProductRecordCount / 25f); + (uint)Math.Ceiling(Program.Database.ProductRecordCount / 25f); if (pageCount != libraryPanel1.PageCount) libraryPanel1.PageCount = pageCount; } private void AddLibraryItems() { - DPCommon.WriteToLog("Add library items."); + // DPCommon.WriteToLog("Add library items."); libraryPanel1.EditMode = true; // Loop while i is less than records count and count is less than 25. for (var i = 0; i < ProductRecords.Length; i++) { - var record = ProductRecords[i]; - var lb = AddNewLibraryItem(record); + DPProductRecord record = ProductRecords[i]; + LibraryItem lb = AddNewLibraryItem(record); lb.ProductRecord = record; lb.Image = File.Exists(record.ThumbnailPath) ? AddReferenceImage(record.ThumbnailPath) @@ -303,15 +308,15 @@ private void AddLibraryItems() private void AddSearchItems() { - DPCommon.WriteToLog("Add search items."); + // DPCommon.WriteToLog("Add search items."); libraryPanel1.EditMode = true; // Loop while i is less than records count and count is less than 25. var startIndex = (libraryPanel1.CurrentPage - 1) * 25; var count = 0; for (var i = startIndex; i < SearchRecords.Length && count < 25; i++, count++) { - var record = SearchRecords[i]; - var lb = AddNewSearchItem(record); + DPProductRecord record = SearchRecords[i]; + LibraryItem lb = AddNewSearchItem(record); lb.ProductRecord = record; lb.Image = File.Exists(record.ThumbnailPath) ? AddReferenceImage(record.ThumbnailPath) @@ -334,7 +339,7 @@ private void searchBox_KeyDown(object sender, KeyEventArgs e) { lastSearchID = (uint)Random.Shared.Next(1, int.MaxValue); lastSearchQuery = searchBox.Text; - DPDatabase.SearchQ(searchBox.Text, SortMethod, callback: OnSearchUpdate); + Program.Database.SearchQ(searchBox.Text, SortMethod, callback: OnSearchUpdate); } } } @@ -358,12 +363,12 @@ private void OnLibraryQueryUpdate(DPProductRecord[] productRecords) if (!searchMode) TryPageUpdate(); } - private void OnAddedProductRecord(DPProductRecord record, uint recordID) + private void OnAddedProductRecord(DPProductRecord record) { - DPCommon.WriteToLog($"A product has been added! {record.ProductName}"); + // DPCommon.WriteToLog($"A product has been added! {record.ProductName}"); // First, check to see if it is in range of the current page. // If it is, then we need to update that page. - if (recordID <= (libraryPanel1.CurrentPage) * 25 && recordID > (libraryPanel1.CurrentPage - 1) * 25) + if (record.ID <= (libraryPanel1.CurrentPage) * 25 && record.ID > (libraryPanel1.CurrentPage - 1) * 25) { var newProductRecords = new DPProductRecord[ProductRecords.Length + 1]; ProductRecords.CopyTo(newProductRecords, 0); @@ -373,13 +378,13 @@ private void OnAddedProductRecord(DPProductRecord record, uint recordID) } // Otherwise, we may need to change the page count and current page. - if ((uint)Math.Ceiling((DPDatabase.ProductRecordCount + 1) / 25f) != libraryPanel1.PageCount) + if ((uint)Math.Ceiling((Program.Database.ProductRecordCount + 1) / 25f) != libraryPanel1.PageCount) { libraryPanel1.NudgePageCount(libraryPanel1.PageCount + 1); // Now we need to update the current page. // If the ID is higher than the current page range, then we don't do anything. // Otherwise, we need to move the current page up one. - if (recordID < libraryPanel1.CurrentPage * 25) + if (record.ID < libraryPanel1.CurrentPage * 25) libraryPanel1.NudgeCurrentPage(libraryPanel1.CurrentPage + 1); } } @@ -394,7 +399,7 @@ private void OnRemovedProductRecord(uint ID) var records = new DPProductRecord[ProductRecords.Length - 1]; Array.ConstrainedCopy(ProductRecords, 0, records, 0, i); Array.ConstrainedCopy(ProductRecords, i + 1, records, i, records.Length - i); - var lb = libraryPanel1.LibraryItems.Find(l => l.ProductRecord == ProductRecords[i]); + LibraryItem lb = libraryPanel1.LibraryItems.Find(l => l.ProductRecord == ProductRecords[i]); if (lb != null) DisableLibraryItem(lb); ProductRecords = records; break; @@ -409,7 +414,7 @@ private void OnRemovedProductRecord(uint ID) var records = new DPProductRecord[SearchRecords.Length - 1]; Array.ConstrainedCopy(SearchRecords, 0, records, 0, i); Array.ConstrainedCopy(SearchRecords, i + 1, records, i, records.Length - i); - var lb = libraryPanel1.LibraryItems.Find(l => l.ProductRecord == SearchRecords[i]); + LibraryItem lb = libraryPanel1.LibraryItems.Find(l => l.ProductRecord == SearchRecords[i]); if (lb != null) DisableLibraryItem(lb); SearchRecords = records; break; @@ -425,7 +430,7 @@ private void OnModifiedProductRecord(DPProductRecord updatedRecord, uint oldID) { var index = Array.FindIndex(SearchRecords, r => r.ID == oldID); if (index == -1) return; - var lb = libraryPanel1.LibraryItems.Find(l => l.ProductRecord == SearchRecords[index]); + LibraryItem lb = libraryPanel1.LibraryItems.Find(l => l.ProductRecord == SearchRecords[index]); if (lb is null) return; UpdateLibraryItem(lb, updatedRecord); SearchRecords[index] = updatedRecord; @@ -434,7 +439,7 @@ private void OnModifiedProductRecord(DPProductRecord updatedRecord, uint oldID) { var index = Array.FindIndex(ProductRecords, r => r.ID == oldID); if (index == -1) return; - var lb = libraryPanel1.LibraryItems.Find(l => l.ProductRecord == ProductRecords[index]); + LibraryItem lb = libraryPanel1.LibraryItems.Find(l => l.ProductRecord == ProductRecords[index]); if (lb is null) return; UpdateLibraryItem(lb, updatedRecord); ProductRecords[index] = updatedRecord; @@ -473,8 +478,8 @@ private void DisableLibraryItem(LibraryItem lb) private void sortByCombo_SelectedIndexChanged(object sender, EventArgs e) { SortMethod = (DPSortMethod)Enum.Parse(typeof(DPSortMethod), sortByCombo.Text); - if (searchMode) DPDatabase.SearchQ(lastSearchQuery, SortMethod, callback: OnSearchUpdate); - else DPDatabase.GetProductRecordsQ(SortMethod, libraryPanel1.CurrentPage, 25, callback: OnLibraryQueryUpdate); + if (searchMode) Program.Database.SearchQ(lastSearchQuery, SortMethod, callback: OnSearchUpdate); + else Program.Database.GetProductRecordsQ(SortMethod, libraryPanel1.CurrentPage, 25, callback: OnLibraryQueryUpdate); } } } diff --git a/src/Forms/MainForm.resx b/src/DAZ_Installer.Windows/Pages/Library.resx similarity index 99% rename from src/Forms/MainForm.resx rename to src/DAZ_Installer.Windows/Pages/Library.resx index dca3f91..1758082 100644 --- a/src/Forms/MainForm.resx +++ b/src/DAZ_Installer.Windows/Pages/Library.resx @@ -57,8 +57,8 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - 17, 17 + + 214, 17 diff --git a/src/Custom Controls/Settings.Designer.cs b/src/DAZ_Installer.Windows/Pages/Settings.Designer.cs similarity index 99% rename from src/Custom Controls/Settings.Designer.cs rename to src/DAZ_Installer.Windows/Pages/Settings.Designer.cs index 1487610..73b060b 100644 --- a/src/Custom Controls/Settings.Designer.cs +++ b/src/DAZ_Installer.Windows/Pages/Settings.Designer.cs @@ -1,5 +1,5 @@  -namespace DAZ_Installer +namespace DAZ_Installer.Windows.Pages { partial class Settings { diff --git a/src/DAZ_Installer.Windows/Pages/Settings.cs b/src/DAZ_Installer.Windows/Pages/Settings.cs new file mode 100644 index 0000000..16e3a79 --- /dev/null +++ b/src/DAZ_Installer.Windows/Pages/Settings.cs @@ -0,0 +1,618 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +using DAZ_Installer.Core; +using DAZ_Installer.Windows.Forms; +using DAZ_Installer.Windows.DP; +using Microsoft.VisualBasic.FileIO; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Windows.Forms; +using Serilog; + +namespace DAZ_Installer.Windows.Pages +{ + public partial class Settings : UserControl + { + public ILogger Logger { get; set; } = Log.Logger.ForContext(); + internal static bool setupComplete { get; set; } = false; + internal static readonly string[] names = new string[] { "Manifest Only", "Manifest and File Sense", "File Sense Only" }; + internal static bool validating { get; set; } = false; + internal static Settings settingsPage { get; set; } = null; + + private const string SETTINGS_PATH = "settings.json"; + + public Settings() + { + Logger.Debug("Creating Settings object"); + InitializeComponent(); + settingsPage = this; + } + + private void Settings_Load(object sender, EventArgs e) + { + Logger.Debug("Settings_Load called"); + Task.Run(LoadSettings).ContinueWith((t) => + { + Logger.Information("Setting up Settings' controls"); + SetupDownloadThumbnailsSetting(); + SetupDestinationPathSetting(); + SetupFileHandling(); + SetupTempPath(); + SetupContentFolders(); + SetupContentRedirects(); + SetupDeleteSourceFiles(); + SetupPreviouslyInstalledProducts(); + SetupAllowOverwriting(); + SetupRemoveAction(); + + loadingPanel.Visible = false; + loadingPanel.Dispose(); + validating = false; + Logger.Information("Settings loaded"); + }); + loadingPanel.Visible = true; + loadingPanel.BringToFront(); + } + + // ._. + private void LoadSettings() + { + Logger.Information("Loading Settings"); + // DPCommon.WriteToLog("Loading settings..."); + // Get our settings. + validating = true; + if (!DPSettings.CurrentSettingsObject.Valid) + DPSettings.CurrentSettingsObject = SetupSettings(); + + ValidateDirectoryPaths(DPSettings.CurrentSettingsObject); + } + public bool SaveSettings() + { + try + { + using StreamWriter file = File.CreateText(SETTINGS_PATH); + file.Write(DPSettings.CurrentSettingsObject.ToJson()); + return true; + } + catch (Exception ex) + { + // DPCommon.WriteToLog($"Failed to parse settings. REASON: {ex}"); + } + return false; + } + + public DPSettings? ParseSettings() + { + var result = DPSettings.FromJson(File.ReadAllText(SETTINGS_PATH)); + if (result == null) + { + MessageBox.Show("There was an error processing settings. Settings have been reset to default values.", + "Failed to process settings", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + return result; + } + + public DPSettings SetupSettings() + { + var settings = new DPSettings(); + if (DPRegistry.ContentDirectories.Length == 0) + { + MessageBox.Show("Couldn't find DAZ Studio Content Directories located in registry. On the next prompt, please select where you want your products to be installed to. You can always change this later in the settings.", + "Content Directories not found", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); + var path = AskForDirectory(); + while (string.IsNullOrEmpty(path)) + { + MessageBox.Show("No directory was selected. It is required that you select a directory for products you wish to install. " + + "Please select where you want your products to be installed to. You can always change this later in the settings.", + "Directory selection required", MessageBoxButtons.OK, MessageBoxIcon.Error); + path = AskForDirectory(); + } + } + else settings.DestinationPath = DPRegistry.ContentDirectories[0]; + return settings; + } + + private void ValidateDirectoryPaths(DPSettings settings) + { + var destExists = !string.IsNullOrEmpty(settings.DestinationPath) && Directory.Exists(settings.DestinationPath); + var thumbExists = !string.IsNullOrEmpty(settings.ThumbnailsDir) && Directory.Exists(settings.ThumbnailsDir); + var tempExists = !string.IsNullOrEmpty(settings.TempDir) && (Directory.Exists(settings.TempDir) || Path.Combine(Path.GetTempPath(), "DazProductInstaller") == settings.TempDir); + var databaseExists = !string.IsNullOrEmpty(settings.DatabaseDir) && Directory.Exists(settings.DatabaseDir); + var anyEmpty = !string.IsNullOrEmpty(settings.DatabaseDir) || + !string.IsNullOrEmpty(settings.TempDir) || + !string.IsNullOrEmpty(settings.ThumbnailsDir) || + !string.IsNullOrEmpty(settings.DestinationPath); + + var invalidSettings = anyEmpty && (!destExists || !thumbExists || !tempExists || !databaseExists); + if (invalidSettings) MessageBox.Show("Some paths are invalid and have been reverted to default.", "Settings defaulted", + MessageBoxButtons.OK, MessageBoxIcon.Information); + applySettingsBtn.Enabled = invalidSettings; + if (!destExists) + { + if (DPRegistry.ContentDirectories.Length == 0) + { + MessageBox.Show("Couldn't find DAZ Studio Content Directories located in registry. On the next prompt, please select where you want your products to be installed to. You can always change this later in the settings.", + "Content Directories not found", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); + var path = AskForDirectory(); + while (string.IsNullOrEmpty(path)) + { + MessageBox.Show("No directory was selected. It is required that you select a directory for products you wish to install. " + + "Please select where you want your products to be installed to. You can always change this later in the settings.", + "Directory selection required", MessageBoxButtons.OK, MessageBoxIcon.Error); + path = AskForDirectory(); + } + } + else + settings.DestinationPath = DPRegistry.ContentDirectories[0]; + } + if (!thumbExists) + { + settings.ThumbnailsDir = "Thumbnails"; + try + { + Directory.CreateDirectory(settings.ThumbnailsDir); + } + catch (Exception ex) + { + // DPCommon.WriteToLog($"Failed to create directories for default thumbnail path. REASON: {ex}"); + } + } + if (!tempExists) + { + settings.TempDir = Path.Combine(Path.GetTempPath(), "DazProductInstaller"); + try + { + Directory.CreateDirectory(settings.TempDir); + } + catch (Exception ex) + { + // DPCommon.WriteToLog($"Failed to create directories for default temp path. REASON: {ex}"); + } + } + if (!databaseExists) + { + settings.DatabaseDir = "Database"; + try + { + Directory.CreateDirectory(settings.DatabaseDir); + } + catch (Exception ex) + { + // DPCommon.WriteToLog($"Failed to create directories for default database path. REASON: {ex}"); + } + } + + } + + private void SetupContentRedirects() + { + foreach (KeyValuePair keypair in DPSettings.CurrentSettingsObject.FolderRedirects) + { + contentFolderRedirectsListBox.Items.Add($"{keypair.Key} --> {keypair.Value}"); + } + } + + private void SetupContentFolders() + { + foreach (var folder in DPSettings.CurrentSettingsObject.CommonContentFolderNames) + { + contentFoldersListBox.Items.Add(folder); + } + } + + private void SetupTempPath() => tempTxtBox.Text = DPSettings.CurrentSettingsObject.TempDir; + + private void SetupDestinationPathSetting() + { + // If no detected daz content paths, all handled in the initalization phase of DPSettings.currentSettingsObject. + // First, we will add our selected path. + destinationPathCombo.Items.Add(DPSettings.CurrentSettingsObject.DestinationPath); + destinationPathCombo.SelectedIndex = 0; + destinationPathCombo.Items.AddRange(DPSettings.CurrentSettingsObject.detectedDazContentPaths); + } + + private void SetupFileHandling() + { + + fileHandlingCombo.Items.AddRange(names); + + // Now show the one we selected. + InstallOptions fileMethod = DPSettings.CurrentSettingsObject.HandleInstallation; + switch (fileMethod) + { + case InstallOptions.ManifestOnly: + fileHandlingCombo.SelectedIndex = 0; + break; + case InstallOptions.ManifestAndAuto: + fileHandlingCombo.SelectedIndex = 1; + break; + case InstallOptions.Automatic: + fileHandlingCombo.SelectedIndex = 2; + break; + } + } + + private void SetupDownloadThumbnailsSetting() + { + + foreach (var option in Enum.GetNames(typeof(SettingOptions))) + { + downloadThumbnailsComboBox.Items.Add(option); + } + + SettingOptions choice = DPSettings.CurrentSettingsObject.DownloadImages; + downloadThumbnailsComboBox.SelectedItem = Enum.GetName(choice); + } + + private void SetupDeleteSourceFiles() + { + foreach (var option in Enum.GetNames(typeof(SettingOptions))) + { + removeSourceFilesCombo.Items.Add(option); + } + + SettingOptions choice = DPSettings.CurrentSettingsObject.PermDeleteSource; + removeSourceFilesCombo.SelectedItem = Enum.GetName(choice); + } + + private void SetupPreviouslyInstalledProducts() + { + foreach (var option in Enum.GetNames(typeof(SettingOptions))) + { + installPrevProductsCombo.Items.Add(option); + } + + SettingOptions choice = DPSettings.CurrentSettingsObject.InstallPrevProducts; + installPrevProductsCombo.SelectedItem = Enum.GetName(choice); + } + + private void SetupAllowOverwriting() + { + foreach (var option in Enum.GetNames(typeof(SettingOptions))) + { + allowOverwritingCombo.Items.Add(option); + } + allowOverwritingCombo.SelectedItem = Enum.GetName(DPSettings.CurrentSettingsObject.OverwriteFiles); + } + + private void SetupRemoveAction() + { + removeActionCombo.Items.AddRange(new string[] { "Delete permanently", "Move to Recycle Bin" }); + switch (DPSettings.CurrentSettingsObject.DeleteAction) + { + case RecycleOption.DeletePermanently: + removeActionCombo.SelectedItem = "Delete permanently"; + return; + default: + removeActionCombo.SelectedItem = "Move to Recycle Bin"; + return; + } + } + private void downloadThumbnailsComboBox_SelectedIndexChanged(object sender, EventArgs e) + { + + } + + private void applySettingsBtn_Click(object sender, EventArgs e) + { + // Update settings. + var updateResult = UpdateSettings(); + + if (!updateResult) return; + + // Try saving settings. + var saveResult = SaveSettings(); + + // If something failed... + if (!saveResult) + MessageBox.Show("An error occurred while saving settings. You're settings have NOT been saved. Please try saving again.", + "Error saving settings", MessageBoxButtons.OK, MessageBoxIcon.Error); + + applySettingsBtn.Enabled = false; + } + + private bool UpdateSettings() + { + // Only get drives that are mounted and ready to use. + // Do we want to limit this to only local drives (eg. not network drives)? + DriveInfo[] drives = Array.FindAll(DriveInfo.GetDrives(), d => d.IsReady); + // We don't update content folders. + if (validating) return false; + var invalidReponses = false; + DPSettings.CurrentSettingsObject.DownloadImages = Enum.Parse((string)downloadThumbnailsComboBox.SelectedItem); + validating = true; + // Destination Path + + DESTCHECK: + // May need to use PathHelper.NormalizePath() here. + var destinationPath = destinationPathCombo.Text.Trim(); + // this solves issue: A loop occurred when the path was G:/ but G:/ was not mounted. + if (Directory.Exists(destinationPath)) DPSettings.CurrentSettingsObject.DestinationPath = destinationPath; + else + { + if (Array.Find(drives, d => d.Name == destinationPath) == null) + { + // Means the drive is not mounted/ready, we need to select at least one + if (DPRegistry.ContentDirectories.Length == 0) + { + MessageBox.Show("The destination path currently selected is not valid because it is not mounted (or ready to be used). Please select a valid destination path in the following prompt.", + "Invalid destination path", MessageBoxButtons.OK, MessageBoxIcon.Error); + DPSettings.CurrentSettingsObject.DestinationPath = destinationPathCombo.Text = AskForDirectory(); + } + else DPSettings.CurrentSettingsObject.DestinationPath = destinationPathCombo.Text = DPRegistry.ContentDirectories[0]; + } + try + { + Directory.CreateDirectory(destinationPath); + goto DESTCHECK; + } + catch (UnauthorizedAccessException ex) + { + if (!HandleDirectoryUnauthorizedException(destinationPath)) + { + MessageBox.Show("Application does not have permission to access the destination path.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + // TODO: Log error. + } + } + catch { } + destinationPathCombo.Text = DPSettings.CurrentSettingsObject.DestinationPath; + invalidReponses = true; + } + + // Temp Path + // TODO: We don't want to delete the temp folder itself. + // For example: We don't want to delete D:/temp, we want to delete D:/temp/. + // The difference is that currently D:/temp will be deleted whereas, + // D:/temp/ will not delete the temp folder but all subfolders and files in it. + var tempPath = tempTxtBox.Text.Trim(); + TEMPCHECK: + if (Directory.Exists(tempPath)) DPSettings.CurrentSettingsObject.TempDir = tempPath; + else + { + try + { + Directory.CreateDirectory(tempPath); + goto TEMPCHECK; + } + catch (IOException ex) + { + if (!HandleDirectoryUnauthorizedException(destinationPath)) + { + // Try resetting to default. + try + { + tempPath = Path.Combine(Path.GetTempPath(), "DazProductInstaller"); + MessageBox.Show("The temp path currently selected is not valid and has been reset to the default temp path.", + "Temp path reset", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + catch + { + MessageBox.Show("The temp path currently selected is not valid. Additionally, the application does not have permission to your system's default temp path.", + "Temp Access Issue", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + MessageBox.Show("The temp path currently selected is not valid because it is not mounted (or ready to be used).", + "Invalid temp path", MessageBoxButtons.OK, MessageBoxIcon.Error); + // TODO: Log error. + } + } + catch { } + tempTxtBox.Text = DPSettings.CurrentSettingsObject.TempDir; + invalidReponses = true; + } + + // File Handling Method + DPSettings.CurrentSettingsObject.HandleInstallation = (InstallOptions)fileHandlingCombo.SelectedIndex; + + //Content Folders + // We want to maintain the comparer. + DPSettings.CurrentSettingsObject.CommonContentFolderNames.Clear(); + DPSettings.CurrentSettingsObject.CommonContentFolderNames.EnsureCapacity(contentFoldersListBox.Items.Count); + for (var i = 0; i < contentFoldersListBox.Items.Count; i++) + { + DPSettings.CurrentSettingsObject.CommonContentFolderNames.Add((string)contentFoldersListBox.Items[i]); + } + + // Alias Content Folders + // We want to maintain the comparer. + DPSettings.CurrentSettingsObject.FolderRedirects.Clear(); + DPSettings.CurrentSettingsObject.FolderRedirects.EnsureCapacity(contentFolderRedirectsListBox.Items.Count); + foreach (string item in contentFolderRedirectsListBox.Items) + { + var tokens = item.Split(" --> "); + DPSettings.CurrentSettingsObject.FolderRedirects[tokens[0]] = tokens[1]; + } + + // Permanate Delete Source + DPSettings.CurrentSettingsObject.PermDeleteSource = (SettingOptions)removeSourceFilesCombo.SelectedIndex; + + // Install Prev Products + DPSettings.CurrentSettingsObject.InstallPrevProducts = (SettingOptions)installPrevProductsCombo.SelectedIndex; + + if (invalidReponses) + { + MessageBox.Show("Some inputs were invalid and were reset to their previous state. See log for more info.", "Invalid inputs", MessageBoxButtons.OK, MessageBoxIcon.Information); + validating = false; + return false; + } + validating = false; + return true; + } + + internal string AskForDirectory() + { + if (InvokeRequired) + { + var result = Invoke(new Func(AskForDirectory)); + return result; + } + else + { + using var folderBrowser = new FolderBrowserDialog(); + folderBrowser.Description = "Select folder for product installs"; + folderBrowser.UseDescriptionForTitle = true; + DialogResult dialogResult = folderBrowser.ShowDialog(); + if (dialogResult == DialogResult.Cancel) return string.Empty; + return folderBrowser.SelectedPath; + } + } + + /// + /// Attempts to fix the unauthorized access exception and later attempts to create the directory (if it doesn't exist). + /// + /// The path of the directory. + /// Whehther the directory is now accessiable. + internal static bool HandleDirectoryUnauthorizedException(string path) + { + // Determine whether the path is a folder or a file path. + DirectoryInfo info = new(path); + try + { + // TODO: Log file attributes. + if (info.Attributes.HasFlag(FileAttributes.ReadOnly)) info.Attributes &= ~FileAttributes.ReadOnly; + } + catch (UnauthorizedAccessException ex) + { + // Unauthorized is not due to the readonly flag but because we literally don't have permission. + // Can't do anything. + } + catch (Exception e) + { + // Ok...something else happened. + } + return Directory.Exists(path); + } + #region UI Event Handlers + private void tempTxtBox_Leave(object sender, EventArgs e) + { + if (!applySettingsBtn.Enabled && tempTxtBox.Text != DPSettings.CurrentSettingsObject.TempDir) + { + applySettingsBtn.Enabled = true; + } + } + + private void tempTxtBox_KeyUp(object sender, KeyEventArgs e) + { + if (!applySettingsBtn.Enabled && tempTxtBox.Text != DPSettings.CurrentSettingsObject.TempDir) + { + applySettingsBtn.Enabled = true; + } + } + + private void destinationPathCombo_Leave(object sender, EventArgs e) + { + if (!applySettingsBtn.Enabled && destinationPathCombo.Text != DPSettings.CurrentSettingsObject.DestinationPath) + { + applySettingsBtn.Enabled = true; + } + } + + private void destinationPathCombo_TextChanged(object sender, EventArgs e) + { + if (!applySettingsBtn.Enabled && destinationPathCombo.Text != DPSettings.CurrentSettingsObject.DestinationPath) + { + applySettingsBtn.Enabled = true; + } + } + + private void downloadThumbnailsComboBox_TextChanged(object sender, EventArgs e) + { + if (!applySettingsBtn.Enabled && downloadThumbnailsComboBox.Text != Enum.GetName(DPSettings.CurrentSettingsObject.DownloadImages)) + { + applySettingsBtn.Enabled = true; + } + } + + private void fileHandlingCombo_TextChanged(object sender, EventArgs e) + { + if (!applySettingsBtn.Enabled && fileHandlingCombo.Text != names[(int)DPSettings.CurrentSettingsObject.HandleInstallation]) + { + applySettingsBtn.Enabled = true; + } + } + + private void removeSourceFiles_TextChanged(object sender, EventArgs e) + { + if (!applySettingsBtn.Enabled && removeSourceFilesCombo.Text != Enum.GetName(DPSettings.CurrentSettingsObject.PermDeleteSource)) + { + applySettingsBtn.Enabled = true; + } + } + + private void installPrevProducts_TextChanged(object sender, EventArgs e) + { + if (!applySettingsBtn.Enabled && installPrevProductsCombo.Text != Enum.GetName(DPSettings.CurrentSettingsObject.InstallPrevProducts)) + { + applySettingsBtn.Enabled = true; + } + } + private void chooseDestPathBtn_Click(object sender, EventArgs e) + { + using var browser = new FolderBrowserDialog(); + browser.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyComputer); + browser.Description = "Select folder to install products into"; + browser.UseDescriptionForTitle = true; + DialogResult result = browser.ShowDialog(); + if (result == DialogResult.OK) + { + destinationPathCombo.Items[0] = browser.SelectedPath; + destinationPathCombo.SelectedIndex = 0; + destinationPathCombo_TextChanged(null, null); + } + } + + private void chooseTempPathBtn_Click(object sender, EventArgs e) + { + using var browser = new FolderBrowserDialog(); + browser.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyComputer); + browser.Description = "Select folder to temporarily extract products into"; + browser.UseDescriptionForTitle = true; + DialogResult result = browser.ShowDialog(); + if (result == DialogResult.OK) + { + tempTxtBox.Text = browser.SelectedPath; + tempTxtBox_Leave(null, null); + } + } + + private void modifyContentFoldersBtn_Click(object sender, EventArgs e) + { + var contentManager = new ContentFolderManager(); + contentManager.ShowDialog(); + contentFoldersListBox.Items.Clear(); + foreach (var item in contentManager.ContentFolders) + { + contentFoldersListBox.Items.Add(item); + } + applySettingsBtn.Enabled = true; + } + + private void modifyContentRedirectsBtn_Click_1(object sender, EventArgs e) + { + var contentManager = new ContentFolderAliasManager(); + contentManager.ShowDialog(); + if (contentManager.AliasListView is null) return; + contentFolderRedirectsListBox.BeginUpdate(); + contentFolderRedirectsListBox.Items.Clear(); + for (var i = 0; i < contentManager.AliasListView.Items.Count; i++) + { + contentFolderRedirectsListBox.Items.Add(contentManager.AliasListView.Items[i].Text); + } + contentFolderRedirectsListBox.EndUpdate(); + applySettingsBtn.Enabled = true; + } + + private void allowOverwritingCombo_TextChanged(object sender, EventArgs e) + { + if (!applySettingsBtn.Enabled && allowOverwritingCombo.Text != Enum.GetName(DPSettings.CurrentSettingsObject.OverwriteFiles)) + { + applySettingsBtn.Enabled = true; + } + } + + private void openDatabaseBtn_Click(object _, EventArgs __) => new DatabaseView().ShowDialog(); + #endregion + } +} diff --git a/src/Custom Controls/Settings.resx b/src/DAZ_Installer.Windows/Pages/Settings.resx similarity index 100% rename from src/Custom Controls/Settings.resx rename to src/DAZ_Installer.Windows/Pages/Settings.resx diff --git a/src/DAZ_Installer.Windows/Program.cs b/src/DAZ_Installer.Windows/Program.cs new file mode 100644 index 0000000..015e0e5 --- /dev/null +++ b/src/DAZ_Installer.Windows/Program.cs @@ -0,0 +1,91 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +using DAZ_Installer.Database; +using DAZ_Installer.Windows.Forms; +using System; +using System.Diagnostics; +using System.Threading; +using System.Windows.Forms; +using Serilog; +using Serilog.Templates; +using System.Reflection; + +namespace DAZ_Installer.Windows +{ + static class Program + { + public static readonly string AppName = Assembly.GetExecutingAssembly().GetCustomAttribute().Product; + public static readonly string AppVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); + public static readonly string Authors = Assembly.GetExecutingAssembly().GetCustomAttribute().Company; + public static readonly string VersionSuffix = "Pre-Alpha"; + public static bool IsRunByIDE => Debugger.IsAttached; + public static readonly DragDropEffects DropEffect = DragDropEffects.All; + public static int MainThreadID { get; private set; } = 0; + public static bool IsOnMainThread => MainThreadID == Environment.CurrentManagedThreadId; + public static DPDatabase Database { get; private set; } = new DPDatabase("Database/db.db"); + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .Enrich.WithThreadId() + .MinimumLevel.Debug() +#if DEBUG + .WriteTo.Debug(SerilogLoggerConstants.Template) +#endif + .WriteTo.Async(a => a.File(SerilogLoggerConstants.Template, "log.txt", + fileSizeLimitBytes: 20 * 1024 * 1024, // 20 MB + rollOnFileSizeLimit: true, + retainedFileCountLimit: 5, + retainedFileTimeLimit: TimeSpan.FromDays(5)), + blockWhenFull: true) + .CreateLogger(); + Log.ForContext(typeof(Program)).Information("Starting application"); + if (CheckInstances()) return; + Thread.CurrentThread.Name = "Main"; + using var mutex = new Mutex(false, "DAZ_Installer Instance"); + mutex.WaitOne(0); + // Set the main thread ID to this one. + MainThreadID = Environment.CurrentManagedThreadId; + Application.SetHighDpiMode(HighDpiMode.SystemAware); + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + try + { + Application.Run(new MainForm()); + } + catch (Exception ex) + { + // TODO: Show error handler here. + Log.ForContext(typeof(Program)).Fatal(ex, "Application shutdown due to fatal error."); + } + finally { + mutex.ReleaseMutex(); + } + } + + /// + /// Checks if there is a instance of the application running. + /// + /// True if there the app is already running, otherwise false. + static bool CheckInstances() + { + using var mutex = new Mutex(false, "DAZ_Installer Instance"); + // Code from: https://saebamini.com/Allowing-only-one-instance-of-a-C-app-to-run/ + var isAnotherInstanceOpen = !mutex.WaitOne(0); + if (isAnotherInstanceOpen) + { + Log.Warning("User attempted to launch another instance of the application."); + MessageBox.Show(null, "Only one instance of Daz Product Installer is allowed!", "Launch cancelled", MessageBoxButtons.OK, MessageBoxIcon.Error); + return true; + } + + mutex.ReleaseMutex(); + return false; + } + } +} diff --git a/src/Properties/Resources - Copy.ignore b/src/DAZ_Installer.Windows/Properties/Resources - Copy.ignore similarity index 100% rename from src/Properties/Resources - Copy.ignore rename to src/DAZ_Installer.Windows/Properties/Resources - Copy.ignore diff --git a/src/Properties/Resources.Designer.cs b/src/DAZ_Installer.Windows/Properties/Resources.Designer.cs similarity index 91% rename from src/Properties/Resources.Designer.cs rename to src/DAZ_Installer.Windows/Properties/Resources.Designer.cs index dc588b5..ca9fa92 100644 --- a/src/Properties/Resources.Designer.cs +++ b/src/DAZ_Installer.Windows/Properties/Resources.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace DAZ_Installer.Properties { +namespace DAZ_Installer.Windows { using System; @@ -39,7 +39,7 @@ internal Resources() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DAZ_Installer.Properties.Resources", typeof(Resources).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DAZ_Installer.Windows.Properties.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; @@ -90,6 +90,16 @@ internal static System.Drawing.Bitmap ArrowUp { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap ArrowUp1 { + get { + object obj = ResourceManager.GetObject("ArrowUp1", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// diff --git a/src/Properties/Resources.resx b/src/DAZ_Installer.Windows/Properties/Resources.resx similarity index 90% rename from src/Properties/Resources.resx rename to src/DAZ_Installer.Windows/Properties/Resources.resx index 4d15fa3..62dddf7 100644 --- a/src/Properties/Resources.resx +++ b/src/DAZ_Installer.Windows/Properties/Resources.resx @@ -122,21 +122,24 @@ ..\Resources\loading.gif;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - ..\..\Assets\ArrowRight.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Assets\ArrowRight.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - ..\..\Assets\ArrowUp.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Assets\ArrowUp.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - ..\..\Assets\ArrowDown.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Assets\ArrowDown.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - ..\..\Assets\NoImageFound.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Assets\NoImageFound.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - ..\..\Assets\logo.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Assets\logo.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Resources\Logo2-256x.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Assets\ArrowUp1.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + \ No newline at end of file diff --git a/src/Properties/Settings.Designer.cs b/src/DAZ_Installer.Windows/Properties/Settings.Designer.cs similarity index 93% rename from src/Properties/Settings.Designer.cs rename to src/DAZ_Installer.Windows/Properties/Settings.Designer.cs index c6ef62a..aaaf4c2 100644 --- a/src/Properties/Settings.Designer.cs +++ b/src/DAZ_Installer.Windows/Properties/Settings.Designer.cs @@ -8,11 +8,11 @@ // //------------------------------------------------------------------------------ -namespace DAZ_Installer.Properties { +namespace DAZ_Installer.Windows.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.1.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.6.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); diff --git a/src/Properties/Settings.settings b/src/DAZ_Installer.Windows/Properties/Settings.settings similarity index 100% rename from src/Properties/Settings.settings rename to src/DAZ_Installer.Windows/Properties/Settings.settings diff --git a/src/Resources/Logo2-256x.png b/src/DAZ_Installer.Windows/Resources/Logo2-256x.png similarity index 100% rename from src/Resources/Logo2-256x.png rename to src/DAZ_Installer.Windows/Resources/Logo2-256x.png diff --git a/src/Resources/favicon.ico b/src/DAZ_Installer.Windows/Resources/favicon.ico similarity index 100% rename from src/Resources/favicon.ico rename to src/DAZ_Installer.Windows/Resources/favicon.ico diff --git a/src/Resources/loading.gif b/src/DAZ_Installer.Windows/Resources/loading.gif similarity index 100% rename from src/Resources/loading.gif rename to src/DAZ_Installer.Windows/Resources/loading.gif diff --git a/src/DAZ_Installer.Windows/favicon.ico b/src/DAZ_Installer.Windows/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..26736b9898a0cfdb3b2f8d878c51a268384adf58 GIT binary patch literal 105768 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 literal 0 HcmV?d00001 diff --git a/src/DAZ_Installer.csproj b/src/DAZ_Installer.csproj deleted file mode 100644 index 952aa43..0000000 --- a/src/DAZ_Installer.csproj +++ /dev/null @@ -1,90 +0,0 @@ - - - - WinExe - net6.0-windows - true - AnyCPU;x64 - True - DAZ_Installer.Program - prompt - favicon.ico - - - - true - D:\Visual Studio Projs\DAZ_Installer\bin\x64\Debug\ - full - true - DEBUG;TRACE - - - - false - D:\Visual Studio Projs\DAZ_Installer\bin\x64\Release\ - full - true - - - - - - - - - - - - - - - - - - - - - True - True - Resources.resx - - - True - True - Settings.settings - - - True - True - Resources.resx - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - - - ResXFileCodeGenerator - Resources.Designer.cs - - - - - - - - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - - - - - - \ No newline at end of file diff --git a/src/DAZ_Installer.sln b/src/DAZ_Installer.sln index 975e24d..9415e3f 100644 --- a/src/DAZ_Installer.sln +++ b/src/DAZ_Installer.sln @@ -3,13 +3,38 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.1.32328.378 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DAZ_Installer", "DAZ_Installer.csproj", "{9C962F73-147D-466A-B7AC-7FEBE4A82A3C}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FBA78DD9-9103-4CD8-A22F-8CA45122DBC4}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DAZ_Installer.Core", "DAZ_Installer.Core\DAZ_Installer.Core.csproj", "{6719331A-0851-4C4A-9C15-435AFD86EFFB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DAZ_Installer.Database", "DAZ_Installer.Database\DAZ_Installer.Database.csproj", "{BBA2E972-AAE1-429A-841E-44DF6BD117C9}" + ProjectSection(ProjectDependencies) = postProject + {6719331A-0851-4C4A-9C15-435AFD86EFFB} = {6719331A-0851-4C4A-9C15-435AFD86EFFB} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DAZ_Installer.UI", "DAZ_Installer.UI\DAZ_Installer.UI.csproj", "{A89C12E1-F4D6-46B6-8B65-1450928F56CD}" + ProjectSection(ProjectDependencies) = postProject + {6719331A-0851-4C4A-9C15-435AFD86EFFB} = {6719331A-0851-4C4A-9C15-435AFD86EFFB} + {BBA2E972-AAE1-429A-841E-44DF6BD117C9} = {BBA2E972-AAE1-429A-841E-44DF6BD117C9} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DAZ_Installer.CoreTests", "DAZ_Installer.CoreTests\DAZ_Installer.CoreTests.csproj", "{A2EFAAB7-395A-43F2-A882-963355ABDE8D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DAZ_Installer.Windows", "DAZ_Installer.Windows\DAZ_Installer.Windows.csproj", "{4CB2A43F-BC49-44A6-B64E-416D59F07156}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DAZ_Installer.IO", "DAZ_Installer.IO\DAZ_Installer.IO.csproj", "{E619A09A-3C2B-4375-8CFC-C1ED7EA3F3FE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DAZ_Installer.IOTests", "DAZ_Installer.IOTests\DAZ_Installer.IOTests.csproj", "{B822AA2E-CDA5-4ADE-9747-8D3DEB4CD807}" +EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "DAZ_Installer.Common", "DAZ_Installer.Common\DAZ_Installer.Common.shproj", "{B52A5C81-D9C5-4B68-A117-B0AEC3DD5D17}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DAZ_Installer.DatabaseTests", "DAZ_Installer.DatabaseTests\DAZ_Installer.DatabaseTests.csproj", "{27F4B70E-1EE2-4FA6-A947-FCFA0284493F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DAZ_Installer.TestingSuiteWindows", "DAZ_Installer.TestingSuiteWindows\DAZ_Installer.TestingSuiteWindows.csproj", "{EE91DB42-30F0-4DB3-A513-AFE8B4DE75F3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,14 +43,78 @@ Global Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9C962F73-147D-466A-B7AC-7FEBE4A82A3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9C962F73-147D-466A-B7AC-7FEBE4A82A3C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9C962F73-147D-466A-B7AC-7FEBE4A82A3C}.Debug|x64.ActiveCfg = Debug|x64 - {9C962F73-147D-466A-B7AC-7FEBE4A82A3C}.Debug|x64.Build.0 = Debug|x64 - {9C962F73-147D-466A-B7AC-7FEBE4A82A3C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9C962F73-147D-466A-B7AC-7FEBE4A82A3C}.Release|Any CPU.Build.0 = Release|Any CPU - {9C962F73-147D-466A-B7AC-7FEBE4A82A3C}.Release|x64.ActiveCfg = Release|x64 - {9C962F73-147D-466A-B7AC-7FEBE4A82A3C}.Release|x64.Build.0 = Release|x64 + {6719331A-0851-4C4A-9C15-435AFD86EFFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6719331A-0851-4C4A-9C15-435AFD86EFFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6719331A-0851-4C4A-9C15-435AFD86EFFB}.Debug|x64.ActiveCfg = Debug|Any CPU + {6719331A-0851-4C4A-9C15-435AFD86EFFB}.Debug|x64.Build.0 = Debug|Any CPU + {6719331A-0851-4C4A-9C15-435AFD86EFFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6719331A-0851-4C4A-9C15-435AFD86EFFB}.Release|Any CPU.Build.0 = Release|Any CPU + {6719331A-0851-4C4A-9C15-435AFD86EFFB}.Release|x64.ActiveCfg = Release|Any CPU + {6719331A-0851-4C4A-9C15-435AFD86EFFB}.Release|x64.Build.0 = Release|Any CPU + {BBA2E972-AAE1-429A-841E-44DF6BD117C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BBA2E972-AAE1-429A-841E-44DF6BD117C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BBA2E972-AAE1-429A-841E-44DF6BD117C9}.Debug|x64.ActiveCfg = Debug|Any CPU + {BBA2E972-AAE1-429A-841E-44DF6BD117C9}.Debug|x64.Build.0 = Debug|Any CPU + {BBA2E972-AAE1-429A-841E-44DF6BD117C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BBA2E972-AAE1-429A-841E-44DF6BD117C9}.Release|Any CPU.Build.0 = Release|Any CPU + {BBA2E972-AAE1-429A-841E-44DF6BD117C9}.Release|x64.ActiveCfg = Release|Any CPU + {BBA2E972-AAE1-429A-841E-44DF6BD117C9}.Release|x64.Build.0 = Release|Any CPU + {A89C12E1-F4D6-46B6-8B65-1450928F56CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A89C12E1-F4D6-46B6-8B65-1450928F56CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A89C12E1-F4D6-46B6-8B65-1450928F56CD}.Debug|x64.ActiveCfg = Debug|Any CPU + {A89C12E1-F4D6-46B6-8B65-1450928F56CD}.Debug|x64.Build.0 = Debug|Any CPU + {A89C12E1-F4D6-46B6-8B65-1450928F56CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A89C12E1-F4D6-46B6-8B65-1450928F56CD}.Release|Any CPU.Build.0 = Release|Any CPU + {A89C12E1-F4D6-46B6-8B65-1450928F56CD}.Release|x64.ActiveCfg = Release|Any CPU + {A89C12E1-F4D6-46B6-8B65-1450928F56CD}.Release|x64.Build.0 = Release|Any CPU + {A2EFAAB7-395A-43F2-A882-963355ABDE8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2EFAAB7-395A-43F2-A882-963355ABDE8D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2EFAAB7-395A-43F2-A882-963355ABDE8D}.Debug|x64.ActiveCfg = Debug|Any CPU + {A2EFAAB7-395A-43F2-A882-963355ABDE8D}.Debug|x64.Build.0 = Debug|Any CPU + {A2EFAAB7-395A-43F2-A882-963355ABDE8D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2EFAAB7-395A-43F2-A882-963355ABDE8D}.Release|Any CPU.Build.0 = Release|Any CPU + {A2EFAAB7-395A-43F2-A882-963355ABDE8D}.Release|x64.ActiveCfg = Release|Any CPU + {A2EFAAB7-395A-43F2-A882-963355ABDE8D}.Release|x64.Build.0 = Release|Any CPU + {4CB2A43F-BC49-44A6-B64E-416D59F07156}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4CB2A43F-BC49-44A6-B64E-416D59F07156}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4CB2A43F-BC49-44A6-B64E-416D59F07156}.Debug|x64.ActiveCfg = Debug|Any CPU + {4CB2A43F-BC49-44A6-B64E-416D59F07156}.Debug|x64.Build.0 = Debug|Any CPU + {4CB2A43F-BC49-44A6-B64E-416D59F07156}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4CB2A43F-BC49-44A6-B64E-416D59F07156}.Release|Any CPU.Build.0 = Release|Any CPU + {4CB2A43F-BC49-44A6-B64E-416D59F07156}.Release|x64.ActiveCfg = Release|Any CPU + {4CB2A43F-BC49-44A6-B64E-416D59F07156}.Release|x64.Build.0 = Release|Any CPU + {E619A09A-3C2B-4375-8CFC-C1ED7EA3F3FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E619A09A-3C2B-4375-8CFC-C1ED7EA3F3FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E619A09A-3C2B-4375-8CFC-C1ED7EA3F3FE}.Debug|x64.ActiveCfg = Debug|Any CPU + {E619A09A-3C2B-4375-8CFC-C1ED7EA3F3FE}.Debug|x64.Build.0 = Debug|Any CPU + {E619A09A-3C2B-4375-8CFC-C1ED7EA3F3FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E619A09A-3C2B-4375-8CFC-C1ED7EA3F3FE}.Release|Any CPU.Build.0 = Release|Any CPU + {E619A09A-3C2B-4375-8CFC-C1ED7EA3F3FE}.Release|x64.ActiveCfg = Release|Any CPU + {E619A09A-3C2B-4375-8CFC-C1ED7EA3F3FE}.Release|x64.Build.0 = Release|Any CPU + {B822AA2E-CDA5-4ADE-9747-8D3DEB4CD807}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B822AA2E-CDA5-4ADE-9747-8D3DEB4CD807}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B822AA2E-CDA5-4ADE-9747-8D3DEB4CD807}.Debug|x64.ActiveCfg = Debug|Any CPU + {B822AA2E-CDA5-4ADE-9747-8D3DEB4CD807}.Debug|x64.Build.0 = Debug|Any CPU + {B822AA2E-CDA5-4ADE-9747-8D3DEB4CD807}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B822AA2E-CDA5-4ADE-9747-8D3DEB4CD807}.Release|Any CPU.Build.0 = Release|Any CPU + {B822AA2E-CDA5-4ADE-9747-8D3DEB4CD807}.Release|x64.ActiveCfg = Release|Any CPU + {B822AA2E-CDA5-4ADE-9747-8D3DEB4CD807}.Release|x64.Build.0 = Release|Any CPU + {27F4B70E-1EE2-4FA6-A947-FCFA0284493F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27F4B70E-1EE2-4FA6-A947-FCFA0284493F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27F4B70E-1EE2-4FA6-A947-FCFA0284493F}.Debug|x64.ActiveCfg = Debug|Any CPU + {27F4B70E-1EE2-4FA6-A947-FCFA0284493F}.Debug|x64.Build.0 = Debug|Any CPU + {27F4B70E-1EE2-4FA6-A947-FCFA0284493F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27F4B70E-1EE2-4FA6-A947-FCFA0284493F}.Release|Any CPU.Build.0 = Release|Any CPU + {27F4B70E-1EE2-4FA6-A947-FCFA0284493F}.Release|x64.ActiveCfg = Release|Any CPU + {27F4B70E-1EE2-4FA6-A947-FCFA0284493F}.Release|x64.Build.0 = Release|Any CPU + {EE91DB42-30F0-4DB3-A513-AFE8B4DE75F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE91DB42-30F0-4DB3-A513-AFE8B4DE75F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE91DB42-30F0-4DB3-A513-AFE8B4DE75F3}.Debug|x64.ActiveCfg = Debug|Any CPU + {EE91DB42-30F0-4DB3-A513-AFE8B4DE75F3}.Debug|x64.Build.0 = Debug|Any CPU + {EE91DB42-30F0-4DB3-A513-AFE8B4DE75F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE91DB42-30F0-4DB3-A513-AFE8B4DE75F3}.Release|Any CPU.Build.0 = Release|Any CPU + {EE91DB42-30F0-4DB3-A513-AFE8B4DE75F3}.Release|x64.ActiveCfg = Release|Any CPU + {EE91DB42-30F0-4DB3-A513-AFE8B4DE75F3}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -33,4 +122,11 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8075B0EB-E9A3-47A6-9542-3DD40C597416} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + DAZ_Installer.Common\DAZ_Installer.Common.projitems*{27f4b70e-1ee2-4fa6-a947-fcfa0284493f}*SharedItemsImports = 5 + DAZ_Installer.Common\DAZ_Installer.Common.projitems*{4cb2a43f-bc49-44a6-b64e-416d59f07156}*SharedItemsImports = 5 + DAZ_Installer.Common\DAZ_Installer.Common.projitems*{6719331a-0851-4c4a-9c15-435afd86effb}*SharedItemsImports = 5 + DAZ_Installer.Common\DAZ_Installer.Common.projitems*{b52a5c81-d9c5-4b68-a117-b0aec3dd5d17}*SharedItemsImports = 13 + DAZ_Installer.Common\DAZ_Installer.Common.projitems*{ee91db42-30f0-4db3-a513-afe8b4de75f3}*SharedItemsImports = 5 + EndGlobalSection EndGlobal diff --git a/src/DP/ContentType.cs b/src/DP/ContentType.cs deleted file mode 100644 index e3b6983..0000000 --- a/src/DP/ContentType.cs +++ /dev/null @@ -1,43 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE -namespace DAZ_Installer.DP { - /// - /// ContentType represents the type of the content for user daz files (DSON user files). - /// - /// For example, type: "wearable" found in a DSON user file should have a ContentType of ContentType.Wearable. - /// - /// - internal enum ContentType - { - Scene, - Scene_Subset, - Hierachical_Material, - Preset_Hierarchical_Pose, - Wearable, - Character, - Figure, - Prop, - Preset_Properties, - Preset_Shape, - Preset_Pose, - Preset_Material, - Preset_Shader, - Preset_Camera, - Preset_Light, - Preset_Render_Settings, - Preset_Simulation_Settings, - Preset_DFormer, - Preset_Layered_Image, - Preset_Puppeteer, - Modifier, // aka morph - UV_Set, - Script, - Library, - Program, - Media, - Document, - Geometry, - DAZ_File, - Unknown - } -} \ No newline at end of file diff --git a/src/DP/DP7zArchive.cs b/src/DP/DP7zArchive.cs deleted file mode 100644 index ec08699..0000000 --- a/src/DP/DP7zArchive.cs +++ /dev/null @@ -1,339 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Text; -using System.IO.Compression; -using System.Windows.Forms; -using IOPath = System.IO.Path; -using DAZ_Installer.Utilities; -using System.Threading.Tasks; -using System.Threading; - -namespace DAZ_Installer.DP { - // Notes: - // Directories are listed with "l -slt" with the D attribute. Files are with the A attribute. - // Attributes: R - Read-only, H - Hidden, S - System, A - Archive (file), D - Directory - // Note for NTFS - N - Normal is not stored on disk, is dynamically generated by OS. - // For more info: https://jpsoft.com/help/attrswitch.htm - Attribute Switches section - // In the event where there is no more room, 7z will stop extracting even if there is more to - // extract. - internal class DP7zArchive : DPAbstractArchive - { - private bool _hasEncryptedFiles = false; - private bool _hasEncryptedHeader = false; - private Process _process = null; - private bool _processHasStarted = false; - private string _arcPassword = string.Empty; - - // Peek phase variables. - private bool _peekErrored = false; - private bool _peekFinished = false; - - // Extract phase variables. - private bool _extractErrored = false; - private bool _extractFinished = false; - - private bool _seekingFiles = false; - - private Entity _lastEntity = new Entity{ }; - - private struct Entity - { - internal string Path; - internal bool isDirectory; - - internal bool IsEmpty => Path == null; - } - - internal DP7zArchive(string _path, bool innerArchive = false, string? relativePathBase = null) : base(_path, innerArchive, relativePathBase) {} - - #region Override Methods - // It's best for 7z to extract everything to the temp directory. - internal override void Extract() - { - mode = Mode.Extract; - if (_processHasStarted && (!_process?.HasExited ?? false)) _process.Kill(); - - // Setup the process to extract ALL files to the temp folder. - var tempFolder = IOPath.Combine(DPProcessor.TempLocation, IOPath.GetFileNameWithoutExtension(Path)); - try - { - Directory.CreateDirectory(tempFolder); - } catch { } - foreach (var file in Contents) - { - file.ExtractedPath = IOPath.Combine(tempFolder, file.Path); - } - _process = Setup7ZProcess(); - _process.StartInfo.ArgumentList.Add("-o" + tempFolder); - StartProcess(); - ProgressCombo?.UpdateText($"Extracting to temp from archive {IOPath.GetFileName(Path)}..."); - if (!SpinWait.SpinUntil(() => _extractFinished, 60 * 1000)) - { - DPCommon.WriteToLog($"Extract timeout exceeded for {ExtractedPath}."); - } - - - } - - - internal override void Peek() - { - mode = Mode.Peek; - _process = Setup7ZProcess(); - StartProcess(); - if (ProgressCombo == null) ProgressCombo = new DPProgressCombo(); - ProgressCombo.ChangeProgressBarStyle(true); - ProgressCombo.UpdateText($"Seeking files in archive {IOPath.GetFileName(Path)}..."); - if (!SpinWait.SpinUntil(() => _peekFinished, 60 * 1000)) - { - DPCommon.WriteToLog($"Peek timeout exceeded for {IOPath.GetFileName(Path)}."); - } - } - - internal override void ReadContentFiles() - { - foreach (var file in DazFiles) - { - // Skip if the file wasn't extracted at all or if it was extracted to temp. - if (!file.WasExtracted || (file.WasExtracted && file.ExtractedPath != file.TargetPath)) continue; - try - { - using (var stream = new FileStream(file.ExtractedPath, FileMode.Open)) - { - if (stream.ReadByte() == 0x1F && stream.ReadByte() == 0x8B) - { - // It is gzipped compressed. - stream.Seek(0, SeekOrigin.Begin); - using (var gstream = new GZipStream(stream, CompressionMode.Decompress)) - { - using (var streamReader = new StreamReader(gstream, Encoding.UTF8, true)) - { - file.ReadContents(streamReader); - } - } - } - else - { - // It is normal text. - stream.Seek(0, SeekOrigin.Begin); - using (var streamReader = new StreamReader(stream, Encoding.UTF8, true)) - { - file.ReadContents(streamReader); - } - } - } - - } - catch (Exception ex) - { - DPCommon.WriteToLog($"An unexpected error occured in ReadContentFiles() for 7z. REASON: {ex}"); - } - } - } - - internal override void ReadMetaFiles() - { - try - { - foreach (var file in DSXFiles) - { - if (file.WasExtracted) - file.CheckContents(); - } - } catch (Exception ex) - { - DPCommon.WriteToLog($"An unexpected error occured in ReadMetaFiles() for 7z. REASON: {ex}"); - } - } - - internal override void ReleaseArchiveHandles() - { - _process?.Kill(true); - _process?.Dispose(); - } - #endregion - private bool StartProcess() - { - try - { - _process.Start(); - _processHasStarted = true; - _process.BeginOutputReadLine(); - _process.BeginErrorReadLine(); - _process.StandardInput.WriteLineAsync(_arcPassword); - } catch { return false; } - return true; - } - - /// - /// Creates a new 7z process object depending on the current mode. - /// If the current mode is Peek, then it will tell 7z to list contents. - /// Otherwise, it will tell 7z to extract contents. - /// - /// A 7z process. - private Process Setup7ZProcess() { - _process?.Dispose(); - Process process = new Process(); - process.StartInfo.FileName = "7za.exe"; - process.StartInfo.UseShellExecute = false; - process.StartInfo.RedirectStandardInput = true; - process.StartInfo.RedirectStandardOutput = true; - process.StartInfo.RedirectStandardError = true; - process.StartInfo.CreateNoWindow = true; - process.EnableRaisingEvents = true; - process.OutputDataReceived += Handle7zOutput; - process.ErrorDataReceived += Handle7zErrors; - - if (mode == Mode.Peek) - process.StartInfo.ArgumentList.Add("l"); - else - process.StartInfo.ArgumentList.Add("x"); - process.StartInfo.ArgumentList.Add("-aoa"); // Overwrite existing files w/o prompt. - process.StartInfo.ArgumentList.Add("-slt"); // Show technical information. - //process.StartInfo.ArgumentList.Add("-bb1"); // Show names of processed files in log. - - if (IsInnerArchive) - process.StartInfo.ArgumentList.Add(ExtractedPath); - else - process.StartInfo.ArgumentList.Add(Path); - return process; - } - - private void Handle7zErrors(object _, DataReceivedEventArgs e) - { - if (e.Data is null) return; - ReadOnlySpan msg = e.Data; - if (msg.Contains("Can not open encrypted archive. Wrong password?")) - { - MessageBox.Show($"Unfortunately, {IOPath.GetFileName(Path)} is encrypted and we " + - "currently do not support encryption yet for 7z files.", "Unsupported encrypted archive", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - DPCommon.WriteToLog($"Handle 7z errors called! Msg: {e.Data}"); - } - - /// - /// Handles the appropriate action when receiving data from StandardOutput. - /// - private void Handle7zOutput(object _, DataReceivedEventArgs e) - { - DPCommon.WriteToLog($"Handle 7z output called! Msg: {e.Data}"); - if (e.Data == null || _hasEncryptedFiles) - { - if (mode == Mode.Peek) _peekFinished = true; - else _extractFinished = true; - if (_hasEncryptedFiles) - { - MessageBox.Show($"Unfortunately, {IOPath.GetFileName(Path)} is encrypted and we " + - "currently do not support encryption yet for 7z files.", "Unsupported encrypted archive", MessageBoxButtons.OK, MessageBoxIcon.Error); - _process.OutputDataReceived -= Handle7zOutput; - _process.ErrorDataReceived -= Handle7zErrors; - } - // Finalize the last 7z content. - if (e.Data is null && !_lastEntity.IsEmpty) - { - FinalizeEntity(); - _lastEntity = new Entity { }; - } - return; - } - ReadOnlySpan msg = e.Data; - if (mode == Mode.Peek) - { - if (msg.StartsWith("----------")) _seekingFiles = true; - if (!_seekingFiles) return; - - if (msg.StartsWith("Path")) - { - if (!_lastEntity.IsEmpty) FinalizeEntity(); - _lastEntity = new Entity { Path = msg.Slice(7).ToString() }; - } - else if (msg.StartsWith("Size")) - { - ulong.TryParse(msg.Slice(7), out ulong size); - TrueArchiveSize += size; - } - else if (msg.StartsWith("Attributes")) - { - var attributes = msg.Slice("Attributes = ".Length); - _lastEntity.isDirectory = attributes.Contains("D"); - } - else if (msg.StartsWith("Encrypted")) - _hasEncryptedFiles = msg.Contains("+"); - else if (msg.Contains("Errors:")) - _peekErrored = true; - } else - { - // Only check if everything really did extract if - var ok = msg.StartsWith("Everything is Ok"); - var errors = msg.Contains("Errors"); - if (!ok && !errors) return; - _extractErrored = errors; - ValidateFilesExtraction(); - // Now retarget and move files from temp to it's apporpriate destination. - ProgressCombo?.UpdateText($"Moving files from archive {IOPath.GetFileName(Path)}..."); - foreach (var file in Contents) - { - if (!file.WasExtracted || !file.WillExtract) continue; - var fileInfo = new FileInfo(file.ExtractedPath); - try - { - Directory.CreateDirectory(IOPath.GetDirectoryName(file.ExtractedPath)); - try - { - fileInfo.MoveTo(file.TargetPath, true); - } catch (UnauthorizedAccessException ex) - { - try - { - var targetInfo = new FileInfo(file.TargetPath); - if (targetInfo.Exists) - targetInfo.Attributes = FileAttributes.Normal; - else DPCommon.WriteToLog($"Failed to move {file.Path} to {file.ExtractedPath} due to unauthorized access and we were not overwriting anything."); - } catch { throw ex; } - } - file.ExtractedPath = file.TargetPath; - file.WasExtracted = true; - } catch (Exception ex) - { - DPCommon.WriteToLog($"Failed to move {file.Path} to {file.ExtractedPath}. REASON: {ex}"); - } - } - _extractFinished = true; - } - } - - private void ValidateFilesExtraction() - { - foreach (var file in Contents) - { - if (file.ExtractedPath == null) continue; - FileInfo fileInfo = new FileInfo(file.ExtractedPath); - file.WasExtracted = fileInfo.Exists; - } - } - - private void FinalizeEntity() - { - if (_lastEntity.isDirectory) - _ = new DPFolder(_lastEntity.Path, null); - else - { - var ext = GetExtension(_lastEntity.Path); - if (DPFile.ValidImportExtension(ext)) - { - var newArchive = CreateNewArchive(_lastEntity.Path, true, RelativePath ?? Path); - newArchive.ParentArchive = this; - } - else - { - var newFile = DPFile.CreateNewFile(_lastEntity.Path, null); - newFile.AssociatedArchive = this; - } - } - } - } -} \ No newline at end of file diff --git a/src/DP/DPAbstractArchive.cs b/src/DP/DPAbstractArchive.cs deleted file mode 100644 index 8d08908..0000000 --- a/src/DP/DPAbstractArchive.cs +++ /dev/null @@ -1,459 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE -using System.Windows.Forms; -using System.Text.RegularExpressions; -using System.Linq; -using System.Collections.Generic; -using IOPath = System.IO.Path; -using System.IO; -using System; - - -namespace DAZ_Installer.DP { - /// - /// Defines the archive type of an archive. - /// - // TODO: Add a type for Multi-Product and Multi-Bundle to help determine whether - // an archive should be added to the database/library - internal enum ArchiveType - { - Product, Bundle, Unknown - } - - /// - /// Defines the archive format of an archive. - /// - internal enum ArchiveFormat { - SevenZ, WinZip, RAR, Unknown - } - /// - /// Abstract class for all supported archive files. - /// Currently the supported archive files are RAR, WinZip, and 7z (partially). - /// - internal abstract class DPAbstractArchive : DPAbstractFile { - /// - /// The current mode of the archive file; describes whether the archie is - /// peeking (seeking files) or extracting (seeking and extracting files). - /// - protected enum Mode { - Peek, Extract - } - /// - /// The name that will be used for the hierachy. Equivalent to Path.GetFileName(Path); - /// - /// The file name of the Path of this archive with the extension. - internal string HierachyName { get; set; } - /// - /// The name that will be used for the list view. The list name is the working archive's FileName + $"\\{Path}". - /// - /// The working archive's FileName + $"\\{Path}". - internal string ListName { get; set; } - /// - /// A list of archives that are children of this archive. - /// - internal List InternalArchives { get; init; } = new List(); - /// - /// The archive that is the parent of this archive. This can be null. - /// - internal DPAbstractArchive? ParentArchive { get; set; } - /// - /// A file that has been detected as a manifest file. This can be null. - /// - internal DPDSXFile? ManifestFile { get; set; } - /// - /// A file that has been detected as a supplement file. This can be null. - /// - internal DPDSXFile? SupplementFile { get; set; } - /// - /// A boolean value to describe if this archive is a child of another archive. Default is false. - /// - internal bool IsInnerArchive { get; set; } = false; - /// - /// The type of this archive. Default is ArchiveType.Unknown. - /// - internal ArchiveType Type { get; set; } = ArchiveType.Unknown; - /// - /// A list of files that errored during the extraction stage. - /// - internal LinkedList ErroredFiles { get; set; } = new LinkedList(); - /// - /// The product info connected to this archive. - /// - internal DPProductInfo ProductInfo = new DPProductInfo(); - - /// - /// A map of all of the folders parented to this archive. - /// - /// The `Path` of the Folder. - /// The folder. - - public Dictionary Folders { get; } = new Dictionary(); - - /// - /// A list of folders at the root level of this archive. - /// - public List RootFolders { get; } = new List(); - /// - /// A list of all of the contents (DPAbstractFiles) in this archive. - /// - /// The file content in this archive. - public List Contents { get; } = new List(); - - /// - /// A list of the root contents/ the contents at root level (DPAbstractFiles) of this archive. - /// - /// The file content in this archive. - public List RootContents { get; } = new List(); - /// - /// A list of all .dsx files in this archive. - /// - /// A file that is either a manifest, supplementary, or support file (.dsx). - internal List DSXFiles { get; } = new List(); - /// - /// A list of all readable daz files in this archive. This consists of types with extension: .duf, .dsf. - /// - /// A file with the extension .duf OR .dsf. - /// - internal List DazFiles { get; } = new List(); - - /// - /// A boolean to determine if the processor can read the contents of the archive without extracting to disk. - /// - internal virtual bool CanReadWithoutExtracting { get; init; } = false; - /// - /// The true uncompressed size of the archive contents in bytes. - /// - internal ulong TrueArchiveSize { get; set; } = 0; - /// - /// The expected tag count for this archive. This value is updated when an applicable file has discovered new tags. - /// - internal uint ExpectedTagCount { get; set; } = 0; - - /// - /// The progress combo that is visible on the extraction page. This is typically null when the file is firsted discovered - /// inside another archive (and therefore before it is processed) and/or after the extraction has completed. - /// - internal DPProgressCombo? ProgressCombo { get; set; } - - /// - /// Identifies what mode the archive is currently in. The default is Mode.Extract. - /// - protected Mode mode { get; set; } = Mode.Extract; - - /// - /// The regex expression used for creating a product name. - /// - /// - internal static Regex ProductNameRegex = new Regex(@"([^+|\-|_|\s]+)", RegexOptions.Compiled); - internal DPAbstractArchive(string _path, bool innerArchive = false, string? relativePathBase = null) : base(_path) - { - IsInnerArchive = innerArchive; // Order matters. - // Make a file but we don't want to check anything. - if (IsInnerArchive) Parent = null; - else _parent = null; - FileName = IOPath.GetFileName(_path); - if (relativePathBase != null) - { - RelativePath = IOPath.GetRelativePath(relativePathBase, Path); - RelativePath = RelativePath.Replace(PathHelper.GetSeperator(RelativePath), PathHelper.GetSeperator(Path)); - } - else RelativePath = FileName; - if (DPProcessor.workingArchive != this && DPProcessor.workingArchive != null) - { - ListName = DPProcessor.workingArchive.FileName + '\\' + Path; - } - Ext = GetExtension(Path); - HierachyName = IOPath.GetFileName(Path); - ProductInfo = new DPProductInfo(IOPath.GetFileNameWithoutExtension(Path)); - - if (IsInnerArchive) - DPProcessor.workingArchive.Contents.Add(this); - } - #region Abstract methods - /// - /// Peeks the archive contents if possible and will extract the archive contents to the destination path. - /// - internal abstract void Extract(); - - /// - /// Previews the archive by discovering files in this archive. - /// - internal abstract void Peek(); - - /// - /// Reads the files listed in DSXFiles. If CanReadWithoutExtracting is true, the file won't be extracted. - /// Otherwise, the file will be extracted to the TEMP_LOCATION of DPProcessor. - /// - internal abstract void ReadMetaFiles(); - - /// - /// Reads files that have the extension .dsf and .duf after it has been extracted. - /// - internal abstract void ReadContentFiles(); - /// - /// Calls the derived archive class to dispose of the file handle. - /// - internal abstract void ReleaseArchiveHandles(); - #endregion - #region Internal Methods - /// - /// Checks whether or not the given ext is what is expected. Checks file headers. - /// - /// Returns an extension of the appropriate archive extraction method. Otherwise, null. - - internal static ArchiveFormat DetermineArchiveFormatPrecise(string location) { - try - { - using FileStream stream = File.OpenRead(location); - var bytes = new byte[8]; - stream.Read(bytes, 0, 8); - stream.Close(); - // ZIP File Header - // 50 4B OR 57 69 - if ((bytes[0] == 80 || bytes[0] == 87) && (bytes[1] == 75 || bytes[2] == 105)) - { - return ArchiveFormat.WinZip; - } - // RAR 5 consists of 8 bytes. 0x52 0x61 0x72 0x21 0x1A 0x07 0x01 0x00 - // RAR 4.x consists of 7. 0x52 0x61 0x72 0x21 0x1A 0x07 0x00 - // Rar! - if (bytes[0] == 82 && bytes[1] == 97 && bytes[2] == 114 && bytes[3] == 33) - { - return ArchiveFormat.RAR; - } - - if (bytes[0] == 55 && bytes[1] == 122 && bytes[2] == 188 && bytes[3] == 175) - { - return ArchiveFormat.SevenZ; - } - return ArchiveFormat.Unknown; - } catch { return ArchiveFormat.Unknown; } - } - - /// - /// Returns an enum describing the archive's format based on the file extension. - /// - /// The path of the archive. - /// A ArchiveFormat enum determining the archive format. - internal static ArchiveFormat DetermineArchiveFormat(string ext) { - // ADDITONAL NOTE: This is called for determing archive files inside of an - // archive file. - ext = ext.ToLower(); - switch (ext) { - case "7z": - return ArchiveFormat.SevenZ; - case "rar": - return ArchiveFormat.RAR; - case "zip": - return ArchiveFormat.WinZip; - default: - if (uint.TryParse(ext, out uint _)) return ArchiveFormat.SevenZ; - return ArchiveFormat.Unknown; - } - } - - internal static DPAbstractArchive CreateNewArchive(string fileName, bool innerArchive = false, string? relativePathBase = null) { - string ext = GetExtension(fileName); - switch (DetermineArchiveFormat(ext)) { - case ArchiveFormat.RAR: - return new DPRARArchive(fileName, innerArchive, relativePathBase); - case ArchiveFormat.SevenZ: - return new DP7zArchive(fileName, innerArchive, relativePathBase); - case ArchiveFormat.WinZip: - return new DPZipArchive(fileName, innerArchive, relativePathBase); - default: - return null; - } - } - - /// - /// Finds files that were supposedly extracted to disk. - /// - /// The file paths of successful extracted files. - private string[] GetSuccessfulFiles() - { - List foundFiles = new List(Contents.Count); - foreach (var file in Contents) { - if (file.WasExtracted) foundFiles.Add(file.Path); - } - return foundFiles.ToArray(); - } - - internal DPProductRecord CreateRecords() - { - string imageLocation = string.Empty; - - // Extraction Record successful folder/file paths will now be relative to their content folder (if any). - var successfulFiles = new List(Contents.Count); - // Folders where a file was extracted underneath it. - // Ex: Content/Documents/a.txt was extracted, therefore "Documents" is added. - var foldersExtracted = new HashSet(Contents.Count); - - foreach (var file in Contents.Where(f => f.WasExtracted)) - { - successfulFiles.Add(file.RelativePath ?? file.Path); - if (file.RelativePath != null) - { - foldersExtracted.Add(IOPath.GetDirectoryName(file.RelativePath)); - } - } - var workingExtractionRecord = - new DPExtractionRecord(IOPath.GetFileName(FileName), DPProcessor.settingsToUse.destinationPath, successfulFiles.ToArray(), ErroredFiles.ToArray(), - null, foldersExtracted.ToArray(), 0); - - if (Type != ArchiveType.Bundle) - { - if (DPProcessor.settingsToUse.downloadImages == SettingOptions.Yes) - { - imageLocation = DPNetwork.DownloadImage(workingExtractionRecord.ArchiveFileName); - } - else if (DPProcessor.settingsToUse.downloadImages == SettingOptions.Prompt) - { - // TODO: Use more reliable method! Support files! - // Pre-check if the archive file name starts with "IM" - if (workingExtractionRecord.ArchiveFileName.StartsWith("IM")) - { - var result = DAZ_Installer.Extract.ExtractPage.DoPromptMessage("Do you wish to download the thumbnail for this product?", "Download Thumbnail Prompt", MessageBoxButtons.YesNo); - if (result == DialogResult.Yes) imageLocation = DPNetwork.DownloadImage(workingExtractionRecord.ArchiveFileName); - } - } - var author = ProductInfo.Authors.Count != 0 ? ProductInfo.Authors.First() : null; - var workingProductRecord = new DPProductRecord(ProductInfo.ProductName, ProductInfo.Tags.ToArray(), author, - null, DateTime.Now, imageLocation, 0, 0); - DPDatabase.AddNewRecordEntry(workingProductRecord, workingExtractionRecord); - return workingProductRecord; - } - return null; - } - - internal DPAbstractFile? FindFileViaNameContains(string name) - { - foreach (var file in Contents) - { - if (file.Path.Contains(name)) return file; - } - return null; - } - - private static string[] ConvertDPFoldersToStringArr(Dictionary folders) - { - string[] strFolders = new string[folders.Count]; - string[] keys = folders.Keys.ToArray(); - for (var i = 0; i < strFolders.Length; i++) - { - strFolders[i] = folders[keys[i]].Path; - } - return strFolders; - } - - /// - /// This function should be called after all the files have been extracted. If no content folders have been found, this is a bundle. - /// - internal ArchiveType DetermineArchiveType() - { - foreach (var folder in Folders.Values) - { - if (folder.isContentFolder) - { - return ArchiveType.Product; - } - } - foreach (var content in Contents) - { - if (content is DPAbstractArchive) return ArchiveType.Bundle; - } - return ArchiveType.Unknown; - } - - internal void GetTags() - { - // First is always author. - // Next is folder names. - var productNameTokens = SplitProductName(); - ReadContentFiles(); - ReadMetaFiles(); - var tagsSet = new HashSet(StringComparer.OrdinalIgnoreCase); - tagsSet.EnsureCapacity(GetEstimateTagCount() + productNameTokens.Length + - (Folders.Count * 2) + ((Contents.Count - InternalArchives.Count) * 2)); - foreach (var file in DazFiles) - { - var contentInfo = file.ContentInfo; - if (contentInfo.Website.Length != 0) tagsSet.Add(contentInfo.Website); - if (contentInfo.Email.Length != 0) tagsSet.Add(contentInfo.Email); - tagsSet.UnionWith(contentInfo.Authors); - } - foreach (var content in Contents) - { - if (content is DPAbstractArchive) continue; - tagsSet.UnionWith(IOPath.GetFileNameWithoutExtension(content.Path).Split(' ')); - } - foreach (var folder in Folders) - { - tagsSet.UnionWith(PathHelper.GetFileName(folder.Key).Split(' ')); - } - tagsSet.UnionWith(ProductInfo.Authors); - tagsSet.UnionWith(productNameTokens); - if (ProductInfo.SKU.Length != 0) tagsSet.Add(ProductInfo.SKU); - if (ProductInfo.ProductName.Length != 0) tagsSet.Add(ProductInfo.ProductName); - ProductInfo.Tags = tagsSet; - - } - - internal int GetEstimateTagCount() { - int count = 0; - foreach (var content in Contents) { - if (content is DPFile) { - count += ((DPFile) content).Tags.Count; - } - } - count += ProductInfo.Authors.Count; - return count; - } - - internal DPFolder FindParent(DPAbstractFile obj) - { - var fileName = PathHelper.GetFileName(obj.Path); - if (fileName == string.Empty) fileName = IOPath.GetFileName(obj.Path.TrimEnd(PathHelper.GetSeperator(obj.Path))); - string relativePathOnly = ""; - try - { - relativePathOnly = PathHelper.GetAbsoluteUpPath(obj.Path.Remove(obj.Path.LastIndexOf(fileName))); - } - catch { } - if (RecursivelyFindFolder(relativePathOnly, out DPFolder folder)) - { - return folder; - } - return null; - } - - internal bool FolderExists(string fPath) => Folders.ContainsKey(fPath); - - internal bool RecursivelyFindFolder(string relativePath, out DPFolder folder) - { - - foreach (var _folder in Folders.Values) - { - if (_folder.Path == relativePath || _folder.Path == PathHelper.SwitchSeperators(relativePath)) - { - folder = _folder; - return true; - } - } - folder = null; - return false; - } - - - internal string[] SplitProductName() { - var matches = ProductNameRegex.Matches(ProductInfo.ProductName); - List tokens = new List(matches.Count); - foreach (Match match in matches) { - tokens.Add(match.Value); - } - return tokens.ToArray(); - } - #endregion - - } -} \ No newline at end of file diff --git a/src/DP/DPAbstractFile.cs b/src/DP/DPAbstractFile.cs deleted file mode 100644 index 84315ed..0000000 --- a/src/DP/DPAbstractFile.cs +++ /dev/null @@ -1,163 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE -using System; -using System.Windows.Forms; -using IOPath = System.IO.Path; - -namespace DAZ_Installer.DP { - /// - /// Abstract class for all elements found in archives (including archives). - /// This means that all files, and archives (which are files) should extend - /// this class. - /// - internal abstract class DPAbstractFile { - /// - /// The file name of a file or folder; equivalent to Path.GetFileName(). - /// - internal string FileName { get; set; } - /// - /// The full path of the file (or folder) in the archive space. - /// However, if the derived type is a DPArchive, the path can be the path - /// in the file system space or archive space. If the archive has IsInnerArchive set to - /// true, then the path is the path of the archive. Otherwise, it is the path in - /// file system space. - /// - internal string Path { get; set; } - /// - /// The full relative path of the file (or folder) relative to the determined content folder (if any). - /// If no content folder is detected, relative path will be null. - /// Currently, relative path is not set for folders. - /// - internal string? RelativePath { get; set; } - /// - /// The relative directory path at which will be used to determine which the file will go to in the system. - /// This property is used to determine the target path of a file. - /// The value will be equal to - /// if the is not in . - /// - internal string RelativeTargetPath { get; set; } - /// - /// The full directory path at which the file go to in the file system. - /// - internal string TargetPath { get; set; } - /// - /// The extension of the file in lowercase characters and without the dot. ext can be empty. - /// - internal string Ext { get; set; } - /// - /// A boolean value to determine if the current file will be extracted. - /// - internal bool WillExtract { get; set; } - /// - /// The folder the file (or folder) is a child of. Can be null. - /// - internal DPFolder? Parent { get => _parent; set => UpdateParent(value); } - /// - /// The location of the file in the file system after it has been extracted. Can be null. - /// - internal string ExtractedPath { get; set; } - /// - /// The unique identifier for the file (or folder). - /// - internal uint UID { get; set; } - /// - /// The associated list view item if any. - /// - internal ListViewItem? AssociatedListItem { get; set; } - /// - /// The associated tree node if any. - /// - internal TreeNode? AssociatedTreeNode { get; set; } - /// - /// A boolean value to determine if the file was successfully extracted. - /// - internal bool WasExtracted { get; set; } - /// - /// A boolean value to determine if this file had errored. - /// - internal bool errored { get; set; } - /// - /// The archive this file is associated to. Can be null. - /// - internal DPAbstractArchive? AssociatedArchive { get; set; } - - protected DPFolder? _parent; - - - /// - /// Updates the parent of the file (or archive). This method is virtual and is overloaded by DPFolder. - /// - /// The folder that will be the new parent of the file (or archive). - internal virtual void UpdateParent(DPFolder? newParent) { - // If we were null, but now we're not... - if (_parent == null && newParent != null) { - // Remove ourselves from root content of the working archive. - try { - DPProcessor.workingArchive.RootContents.Remove(this); - } catch {} - - // Call the folder's addChild() to add ourselves to the children list. - newParent.addChild(this); - _parent = newParent; - } else if (_parent == null && newParent == null) { - // Try to find a parent. - - var potParent = DPProcessor.workingArchive == null ? null : - DPProcessor.workingArchive.FindParent(this); - - // If we found a parent, then update it. This function will be called again. - if (potParent != null) { - Parent = potParent; - } else { - // Otherwise, create a folder for us. - potParent = DPFolder.CreateFolderForFile(Path); - - // If we have successfully created a folder for us, then update it. This function will be called again. - if (potParent != null) Parent = potParent; - else { // Otherwise, we are supposed to be at root. - _parent = null; - if (!DPProcessor.workingArchive.RootContents.Contains(this)) { - DPProcessor.workingArchive.RootContents.Add(this); - } - } - } - } else if (_parent != null && newParent != null) { - // Remove ourselves from previous parent children. - _parent.removeChild(this); - - // Add ourselves to new parent's children. - newParent.addChild(this); - - _parent = newParent; - } else if (_parent != null && newParent == null) { - // Remove ourselves from previous parent's children. - _parent.removeChild(this); - - // Add ourselves to the archive's root content list. - DPProcessor.workingArchive.RootContents.Add(this); - _parent = newParent; - } - } - - /// - /// Returns the extension of a given name without the dot and lowered to all lowercase. - /// - /// The name to get the extension from. - internal static string GetExtension(ReadOnlySpan name) - { - var ext = IOPath.GetExtension(name); - if (ext.Length > 0) ext = ext.Slice(1); - Span lowerExt = new char[ext.Length]; - ext.ToLower(lowerExt, System.Globalization.CultureInfo.CurrentCulture); - return ext.ToString(); - } - - internal DPAbstractFile(string _path) { - UID = DPIDManager.GetNewID(); - Path = _path; - FileName = IOPath.GetFileName(_path); - } - - - } -} \ No newline at end of file diff --git a/src/DP/DPCache.cs b/src/DP/DPCache.cs deleted file mode 100644 index 0e4ac8a..0000000 --- a/src/DP/DPCache.cs +++ /dev/null @@ -1,57 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE -using System.Collections.Generic; - -namespace DAZ_Installer.DP -{ - /// - /// A special dictionary that extends Dictionary and includes a Queue to restrict cache size. - /// Objects do not save based on get calls but rather add calls. If the capacity is reached, - /// the last inserted item will be removed even if it has been called alot. - /// - /// - /// - // TODO: Make so objects are reserved by access calls. - internal class DPCache : Dictionary - { - private const byte MAX_CACHE_SIZE = 25; - private Queue _cache = new Queue(MAX_CACHE_SIZE); - public DPCache() : base(MAX_CACHE_SIZE) { } - - // Hiding is intended: use new. - public new void Add(TKey key, TValue value) - { - if (Count == MAX_CACHE_SIZE) - { - var keyToRemove = _cache.Dequeue(); - Remove(keyToRemove); - } - base.Add(key, value); - _cache.Enqueue(key); - } - - // Hiding is intended: use new. - public new TValue this[TKey key] - { - get => base[key]; - set - { - if (ContainsKey(key)) base[key] = value; - else - { - this.Add(key, value); - } - - } - } - - // Hiding is intended: use new. - public new void Clear() - { - Clear(); - _cache.Clear(); - } - - - } -} diff --git a/src/DP/DPDSXElementCollection.cs b/src/DP/DPDSXElementCollection.cs deleted file mode 100644 index 05507bc..0000000 --- a/src/DP/DPDSXElementCollection.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace DAZ_Installer.DP { - internal class DPDSXElementCollection - { - internal Dictionary> selfClosingElements = new Dictionary>(3); - internal Dictionary> nonSelfClosingElements = new Dictionary>(3); - private int count = 0; - internal void AddElement(DPDSXElement element) - { - count++; - var newLinkedList = new LinkedList(); - if (element.IsSelfClosingElement) - { - if (!selfClosingElements.TryAdd(element.TagName, newLinkedList)) { - selfClosingElements[element.TagName].AddLast(element); - } - newLinkedList.AddFirst(element); - } - else - { - if (!nonSelfClosingElements.TryAdd(element.TagName, newLinkedList)) { - nonSelfClosingElements[element.TagName].AddLast(element); - } else { - newLinkedList.AddFirst(element); - } - } - } - - internal DPDSXElement[] GetAllElements() - { - List elements = new List(count); - foreach (var list in selfClosingElements.Values) { - elements.AddRange(list); - } - foreach (var list in nonSelfClosingElements.Values) { - elements.AddRange(list); - } - return elements.ToArray(); - } - - internal DPDSXElement[] FindElementViaTag(string tagName) - { - if (nonSelfClosingElements.ContainsKey(tagName)) { - return new List(nonSelfClosingElements[tagName]).ToArray(); - } - if (selfClosingElements.ContainsKey(tagName)) { - return new List(selfClosingElements[tagName]).ToArray(); - } - return Array.Empty(); - } - } -} diff --git a/src/DP/DPDSXFile.cs b/src/DP/DPDSXFile.cs deleted file mode 100644 index 1d4a506..0000000 --- a/src/DP/DPDSXFile.cs +++ /dev/null @@ -1,88 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE -using System; -using IOPath = System.IO.Path; -using System.IO; -using System.Collections.Generic; -namespace DAZ_Installer.DP { - /// - /// A special class that marks the DPFile as .dsx file which typically is a Supplement file or a Manifest file. - /// - internal class DPDSXFile : DPFile { - internal bool isSupplementFile, isManifestFile = false; - internal bool isSupportingFile = false; - internal bool contentChecked = false; - internal DPContentInfo ContentInfo = new DPContentInfo(); - - internal DPDSXFile(string _path, DPFolder? __parent) : base(_path, __parent) { - DPProcessor.workingArchive.DSXFiles.Add(this); - Tags.Clear(); // Do not include us. - } - - /// - /// Reads the contents of this file and updates the ContentInfo variables. - /// - internal void CheckContents() { - var parser = new DPDSXParser(ExtractedPath); - var collection = parser.GetDSXFile(); - var search = collection.FindElementViaTag("ProductName"); - if (search.Length != 0) { - AssociatedArchive.ProductInfo.ProductName = search[0].attributes["VALUE"]; - } - search = collection.FindElementViaTag("Artist"); - foreach (var artist in search) { - ContentInfo.Authors.Add(artist.attributes["VALUE"]); - AssociatedArchive.ProductInfo.Authors.Add(artist.attributes["VALUE"]); - } - search = collection.FindElementViaTag("ProductToken"); - if (search.Length != 0) { - ContentInfo.ID = search[0].attributes["VALUE"]; - AssociatedArchive.ProductInfo.SKU = ContentInfo.ID; - } - contentChecked = true; - } - - /// - /// Opens the manifest file, and returns a map with the keys being the full path of the file and the value being the path without "Content\" included. - /// This is only for Daz Product since they require this manifest. - /// - /// - /// Returns a dictionary containing files to extract and their destination. - /// Key is the file path in the archive, and value is the path relative to Content folder. - /// - internal Dictionary GetManifestDestinations() { - var dict = new Dictionary(); - try { - var parser = new DPDSXParser(ExtractedPath); - var collection = parser.GetDSXFile(); - var elements = collection.GetAllElements(); - dict.EnsureCapacity(elements.Length); - foreach (var element in elements) { - if (element.attributes.ContainsKey("ACTION") && new string(element.TagName) == "File") { - var target = element.attributes["TARGET"]; - if (target == "Content") - { - // Get value. - ReadOnlySpan filePath = element.attributes["VALUE"]; - var pathWithoutContent = filePath.Slice(7).TrimStart(PathHelper.GetSeperator(filePath)); - // dict[filePath.ToString()] = IOPath.Combine(DPProcessor.TempLocation, pathWithoutContent); - dict[filePath.ToString()] = pathWithoutContent.ToString(); - } - else if (target == "Application") - { - DPCommon.WriteToLog("Target was application."); - } - } - } - } catch (Exception ex) { - DPCommon.WriteToLog($"An unexpected error occurred while attempting to determine destination paths through the manifest. REASON: {ex}"); - } - return dict; - - } - - public static void main(string[] args) { - var f = new DPDSXFile("", null); - } - } -} \ No newline at end of file diff --git a/src/DP/DPDatabase.Public.cs b/src/DP/DPDatabase.Public.cs deleted file mode 100644 index e1a143f..0000000 --- a/src/DP/DPDatabase.Public.cs +++ /dev/null @@ -1,334 +0,0 @@ -using System; -using System.Data; -using System.Collections.Generic; -using System.Data.SQLite; - -namespace DAZ_Installer.DP -{ - public static partial class DPDatabase - { - // This section is set up as an interface for other classes. You should use these methods - // to get data. These methods can callback if a callback is specified and emit an event. - // If you want to listen through an event, pass a constant caller id. - // Example: a constant caller ID for DPLibrary = 3. - #region Public methods - // TO DO: Improve. This can be so much more efficient. - // I lack the brain capacity to do this at the moment. - - /// - /// Search asynchroniously does a database search based on a user search query (ex: "hello world"). Do not - /// use this function for regex expressions; Instead use RegexSearch. This function does not return - /// the search results but instead returns it to the callback and/or an event. If you wish to retrieve - /// the values through an event, use a caller ID to identify the event is for that particular class at that - /// time. - /// - /// A user search query. - /// The sorting method to apply for results. - /// A caller id for classifying event invocations. - /// The function to return values to. - public static void SearchQ(string searchQuery, DPSortMethod sortMethod = DPSortMethod.None, - uint callerID = 0, Action callback = null) - { - // We only want to do searches on one thread. Calling priority task manager ensures - // we only do searches on one thread. - _priorityTaskManager.Stop(); - _priorityTaskManager.AddToQueue((t) => { - var results = DoSearchS(searchQuery, sortMethod, null, t); - callback?.Invoke(results); - SearchUpdated?.Invoke(results, callerID); - }); - } - - /// - /// RegexSearch asynchroniously does a database search based on a user regex pattern (ex: "[^abc]"). - /// This function does not return the search results but instead returns it to the callback and/or an event. - /// If you wish to retrieve the values through an event, use a caller ID to identify the event is for that particular - /// class at that time. - /// - /// A regex expression. - /// The sorting method to apply for results. - /// A caller id for classifying event invocations. - /// The function to return values to. - public static void RegexSearchQ(string regex, DPSortMethod sortMethod = DPSortMethod.None, - uint callerID = 0, Action callback = null) - { - _priorityTaskManager.Stop(); - _priorityTaskManager.AddToQueue((t) => { - var results = DoRegexSearchS(regex, sortMethod, null, t); - callback?.Invoke(results); - SearchUpdated?.Invoke(results, callerID); - }); - } - /// - /// Gets product records on the page specified by . The page is determined by the - /// limit of . This means that if there are 50 records, and the limit is 10, the max - /// amount of pages is 5. This function does not return any results but will return the results via the callback - /// and via MainQueryCompleted event. - /// - /// The sorting method to apply to the results. - /// The page to get product records from. - /// The max amount of product records per page; the max amount of records to receive. - /// A caller id for classifying event invocations. - /// The function to return values to. - public static void GetProductRecordsQ(DPSortMethod sortMethod, uint page = 1, uint limit = 0, - uint callerID = 0, Action callback = null) - { - _priorityTaskManager.Stop(); - _priorityTaskManager.AddToQueue((t) => { - var results = DoLibraryQuery(page, limit, sortMethod, null, t); - callback?.Invoke(results); - MainQueryCompleted?.Invoke(callerID); - }); - } - - /// - /// Stops the pending chain of main queries such as insert, update, and delete queries. - /// - public static void StopMainDatabaseOperations() => _mainTaskManager.Stop(); - - /// - /// Stops the pending chain of main queries such as insert, update, delete, and get queries. - /// This also stops pending search queries and "view" queries. - /// - public static void StopAllDatabaseOperations() - { - _mainTaskManager.Stop(); - _priorityTaskManager.Stop(); - } - - #endregion - #region Queryable methods - - /// - /// If `forceRefresh` is false, the refresh action will be queued. Otherwise, the action queue will be cleared and database will be refreshed immediately. - /// - /// Refreshes immediately if True, otherwise it is queued. - public static void RefreshDatabaseQ(bool forceRefresh = false) - { - if (forceRefresh) - { - _mainTaskManager.Stop(); - _priorityTaskManager.Stop(); - Initalized = false; - Initialize(); - _columnsCache.Clear(); - } - else - { - _mainTaskManager.AddToQueue(RefreshDatabase); - } - } - - /// - /// Returns all the values from the table specified by the table via the - /// callback or via ViewUpdated event. This may return null. - /// - /// The table name to - /// A caller id for classifying event invocations. - /// The function to return the dataset to. - public static void ViewTableQ(string tableName, uint callerID = 0, Action callback = null) - { - _mainTaskManager.AddToQueue((t) => { - var result = GetAllValuesFromTable(tableName, null, t); - callback?.Invoke(result); - ViewUpdated?.Invoke(result, callerID); - }); - } - /// - /// Queries/Adds a new product record to the database. - /// - /// The new product record to add. - public static void AddNewRecordEntry(DPProductRecord pRecord) - { - _mainTaskManager.AddToQueue(InsertRecords, pRecord, null as DPExtractionRecord, null as SQLiteConnection); - } - /// - /// Queries/Adds a new product and extraction record to the database. - /// - /// The new product record to add. - /// The new extraction record to add. - public static void AddNewRecordEntry(DPProductRecord pRecord, DPExtractionRecord eRecord) - { - _mainTaskManager.AddToQueue(InsertRecords, pRecord, eRecord, null as SQLiteConnection); - } - /// - /// Inserts a new row to the table specified by . It requires the columns that - /// new values will be inserted into. Columns and values length must match. - /// For example, if you want to insert a new row to ProductRecors but only want to insert the name for now, - /// columns would be {"Name"} and values would be {"poppy stick"}. Columns do not have to match - /// the columns in the database, but should include the required, non-null columns. - /// - /// The table to insert values into. - /// The values to insert. - /// The corresponding columns to insert values to. - public static void InsertNewRowQ(string tableName, object[] values, string[] columns) - { - _mainTaskManager.AddToQueue(InsertValuesToTable, tableName, columns, values, null as SQLiteConnection); - } - /// - /// Removes a row by it's ID at the table specifed by . - /// - /// The table to insert values into. - /// The ID of the row to remove. - public static void RemoveRowQ(string tableName, int id) - { - var arg = new Tuple[1] { new Tuple("ID", id) }; - _mainTaskManager.AddToQueue(RemoveValuesWithCondition, tableName, arg, false, null as SQLiteConnection); - } - - public static void RemoveProductRecord(DPProductRecord record, Action callback = null) - { - _mainTaskManager.AddToQueue((t) => - { - var arg = new Tuple[1] { new Tuple("ID", Convert.ToInt32(record.ID)) }; - var success = RemoveValuesWithCondition("ProductRecords", arg, false, null as SQLiteConnection, t); - if (success) - { - callback?.Invoke(record.ID); - ProductRecordRemoved?.Invoke(record.ID); - ExtractionRecordRemoved?.Invoke(record.EID); - } - }); - } - - /// - /// Removes all the values from the table. Triggers in the database are temporarly disabled for deleting. - /// - /// - public static void ClearTableQ(string tableName) - { - _mainTaskManager.AddToQueue(RemoveAllFromTable, tableName, null as SQLiteConnection); - } - - /// - /// Not fully implemented. Do not use. - /// - /// - /// - /// - public static void UpdateValuesQ(string tableName, object[] values, string[] columns, int id) - { - _mainTaskManager.AddToQueue(UpdateValues, tableName, columns, values, id, null as SQLiteConnection); - } - - /// - /// Updates a product record and extraction record. This is currently used for applying changes from the product - /// record form. - /// - /// - /// - /// - public static void UpdateRecordQ(uint id, DPProductRecord newProductRecord, DPExtractionRecord newExtractionRecord, Action callback = null) - { - _mainTaskManager.AddToQueue(t => - { - var success = UpdateProductRecord(id, newProductRecord, null, t); - if (!success) return; - success = UpdateExtractionRecord(id, newExtractionRecord, null, t); - if (!success) return; - callback?.Invoke(newProductRecord.ID); - ProductRecordModified?.Invoke(newProductRecord, id); - ExtractionRecordModified?.Invoke(newExtractionRecord, id); - }); - } - - /// - /// Removes all product records (and corresonding extraction records) from the database that contain a tag - /// specified in tags. Basically, for every record in product records, if the product record contains ANY - /// of the tags specified in , it is removed from the database. - /// - /// Product records' tags to specify for deletion. - public static void RemoveProductRecordsViaTagsQ(string[] tags) - { - // i suck at english. - _mainTaskManager.AddToQueue(RemoveProductRecordsViaTag, tags, null as SQLiteConnection); - } - /// - /// Removes all product records that satisfy the condition specified by . - /// - /// The prerequisite for removing a row that must be met. - public static void RemoveProductRecordsQ(Tuple condition) - { - var t = new Tuple[] { condition }; - _mainTaskManager.AddToQueue(RemoveValuesWithCondition, "ProductRecords", t, false, null as SQLiteConnection); - } - /// - /// Removes all product records that satisfy the conditions specified by . - /// - /// The prerequisites for removing a row that must be met. - public static void RemoveProductRecordsQ(Tuple[] conditions) - { - _mainTaskManager.AddToQueue(RemoveValuesWithCondition, "ProductRecords", conditions, false, null as SQLiteConnection); - } - /// - /// Removes all rows that satisfy the condition specified by . - /// - /// The table to remove rows from. - /// The prerequisite for removing a row that must be met. - public static void RemoveRowWithConditionQ(string tableName, Tuple condition) - { - var t = new Tuple[] { condition }; - _mainTaskManager.AddToQueue(RemoveValuesWithCondition, tableName, t, false, null as SQLiteConnection); - } - /// - /// Removes all rows that satisfy the conditions specified by . - /// - /// The table to remove rows from. - /// The prerequisite for removing a row that must be met. - public static void RemoveRowWithConditionsQ(string tableName, Tuple[] conditions) - { - _mainTaskManager.AddToQueue(RemoveValuesWithCondition, tableName, conditions, false, null as SQLiteConnection); - } - /// - /// Removes all product and extraction records from the database. - /// - public static void RemoveAllRecordsQ() - { - _mainTaskManager.AddToQueue(RemoveAllRecords, null as SQLiteConnection); - } - - /// - /// Removes all tags associated with the product ID specified by from the database. - /// - /// - public static void RemoveTagsQ(uint pid) - { - _mainTaskManager.AddToQueue(RemoveTags, pid, null as SQLiteConnection); - } - /// - /// Gets the extraction records associated with the extraction record ID specified by - /// from the database. This function returns the records via the callback function or via the RecordQueryCompleted - /// event. This may return null if the record does not exist in the database or an internal error occurred. - /// - /// The extraction record ID to get. - /// A caller id for classifying event invocations. - /// The function to return values to. - public static void GetExtractionRecordQ(uint eid, uint callerID = 0, Action callback = null) - { - _priorityTaskManager.AddToQueue((t) => { - var result = GetExtractionRecord(eid, null, t); - callback?.Invoke(result); - RecordQueryCompleted?.Invoke(result, callerID); - }); - } - /// - /// Updates the static ArchiveFileNames variable and returns it via callback function. - /// It returns a unique set of installed archive names. - /// - /// The function to return values to. - public static void GetInstalledArchiveNamesQ(Action> callback = null) - { - _priorityTaskManager.AddToQueue((t) => { - var result = GetArchiveFileNameList(null, t); - callback?.Invoke(result); - }); - } - #endregion - #region Private - private static void OnTimeout() - { - - } - #endregion - } -} diff --git a/src/DP/DPExtractJob.cs b/src/DP/DPExtractJob.cs deleted file mode 100644 index aca3188..0000000 --- a/src/DP/DPExtractJob.cs +++ /dev/null @@ -1,111 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Forms; - - -namespace DAZ_Installer.DP -{ - class DPExtractJob - { - internal string[] filesToProcess { get; init; } - internal bool completed { get; set; } = false; - internal HashSet doNotProcess { get; } = new HashSet(); - internal static DPExtractJob workingJob { get; set; } - internal static DPTaskManager extractJobs = new DPTaskManager(); - internal static List jobs { get; } = new List(3); - internal HashSet filesToNotProcess { get; } = new HashSet(); - // TODO: Check if a product is already in list. - - //internal DPExtractJob(string[] files) - //{ - // filesToProcess = files; - //} - - internal DPExtractJob(ListView.ListViewItemCollection files) - { - jobs.Add(this); - List _files = new List(files.Count); - - foreach (ListViewItem file in files) - { - _files.Add(file.Text); - } - filesToProcess = _files.ToArray(); - } - - internal void DoJob() - { - if (workingJob != null && !workingJob.completed && jobs.IndexOf(this) != 0) - { - SpinWait.SpinUntil(() => workingJob == null && jobs.IndexOf(this) == 0, -1); - } - ParameterizedThreadStart x = new ParameterizedThreadStart(ProcessListAsync); - var thread = new Thread(x); - thread.Name = "DP Extract Job"; - thread.Start(filesToProcess); - if (workingJob != null) - { - workingJob.completed = false; - workingJob = this; - } - else - { - workingJob = this; - workingJob.completed = false; - } - } - - private void ProcessListAsync(object _arr) - { - string[] arr = (string[])_arr; - var progressCombo = new DPProgressCombo(); - // Snapshot the settings and this will be what we use - // throughout the entire extraction process. - var settings = DPSettings.GetCopy(); - for (var i = 0; i < arr.Length; i++) - { - if (DPProcessor.doNotProcessList.IndexOf(Path.GetFileName(arr[i])) != -1) continue; - - var x = arr[i]; - progressCombo.ProgressBar.Value = (int)((double)i / arr.Length * 100); - progressCombo.UpdateText($"Processing archive {i+1}/{arr.Length}: {Path.GetFileName(x)}...({progressCombo.ProgressBar.Value})%"); - DPProcessor.ProcessArchive(x, settings); - } - progressCombo.UpdateText($"Finished processing archives"); - progressCombo.ProgressBar.Value = 100; - var removeFiles = () => - { - foreach (var path in arr) - { - try - { - if (File.Exists(path)) File.Delete(path); - } - catch (Exception ex) { DPCommon.WriteToLog($"Failed to delete source: {path}. REASON: {ex}"); } - } - }; - switch (settings.permDeleteSource) - { - case SettingOptions.Yes: - removeFiles(); - break; - case SettingOptions.Prompt: - var result = MessageBox.Show("Do you wish to PERMENATELY delete all of the source files regardless if it was extracted or not? This cannot be undone.", - "Delete soruce files", MessageBoxButtons.YesNo, MessageBoxIcon.Question); - if (result == DialogResult.Yes) removeFiles(); - break; - } - workingJob.completed = true; - jobs.Remove(this); - workingJob = null; - GC.Collect(); - //DoOtherJobs(); - } - } -} diff --git a/src/DP/DPFile.cs b/src/DP/DPFile.cs deleted file mode 100644 index a9ca54b..0000000 --- a/src/DP/DPFile.cs +++ /dev/null @@ -1,131 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using System.IO; -using IOPath = System.IO.Path; -using System.IO.Compression; - -namespace DAZ_Installer.DP -{ - internal class DPFile : DPAbstractFile - { - - // Public static members - private static Dictionary enumPairs { get; } = new Dictionary(Enum.GetValues(typeof(ContentType)).Length); - public static readonly HashSet DAZFormats = new HashSet() { "duf", "dsa", "dse", "daz", "dsf", "dsb", "dson", "ds", "dsb", "djl", "dsx", "dsi", "dcb", "dbm", "dbc", "dbl", "dso", "dsd", "dsv" }; - public static readonly HashSet GeometryFormats = new HashSet() { "dae", "bvh", "fbx", "obj", "dso", "abc", "mdd", "mi", "u3d" }; - public static readonly HashSet MediaFormats = new HashSet() { "png", "jpg", "hdr", "hdri", "bmp", "gif", "webp", "eps", "raw", "tiff", "tif", "psd", "xcf", "jpeg", "cr2", "svg", "apng", "avif" }; - public static readonly HashSet DocumentFormats = new HashSet() { "txt", "pdf", "doc", "docx", "odt", "html", "ppt", "pptx", "xlsx", "xlsm", "xlsb", "rtf" }; - public static readonly HashSet OtherFormats = new HashSet() { "exe", "lib", "dll", "bat", "cmd" }; - public static readonly HashSet AcceptableImportFormats = new HashSet() { "rar", "zip", "7z", "001" }; - public static Dictionary DPFiles = new Dictionary(); - - // TODO: Do something with this usage of tags. - /// - /// Currently just the file name split by whitespace. - /// - internal List Tags { get; set; } - /// - /// Parent of current file. When setting parent to a folder, property will call addChild() and handle contents appropriately. - /// - internal string ListName { get; set; } - - - // TO DO : Add get tags func. - // TO DO: Add static function to search for a property. - static DPFile() { - foreach (var eName in Enum.GetNames(typeof(ContentType))) - { - var lowercasedName = eName.ToLower(); - enumPairs[lowercasedName] = (ContentType)Enum.Parse(typeof(ContentType), eName); - } - } - - public DPFile(string _path, DPFolder __parent) : base(_path) - { - WillExtract = false; - Parent = __parent; - if (Path != null | Path != "") - { - // _ext can have length of 0, ex: LICENSE - Ext = GetExtension(Path); - } - ListName = DPProcessor.workingArchive.FileName + '\\' + Path; - DPFiles.TryAdd(Path, this); - DPProcessor.workingArchive.Contents.Add(this); - - InitializeTagsList(); - } - - internal static DPFile CreateNewFile(string path, DPFolder? parent) { - var ext = GetExtension(path); - if (ext == "dsf" || ext == "duf") { - return new DPDazFile(path, parent); - } else if (ext == "dsx") { - return new DPDSXFile(path, parent); - } - return new DPFile(path, parent); - } - - - - public static ContentType GetContentType(string type, DPFile dP) - { - if (!string.IsNullOrEmpty(type) && enumPairs.TryGetValue(type, out ContentType contentType)) - { - return contentType; - } - if (dP is null) return ContentType.DAZ_File; - if (GeometryFormats.Contains(dP.Ext)) - { - return ContentType.Geometry; - } - else if (MediaFormats.Contains(dP.Ext)) - { - return ContentType.Media; - } - else if (DocumentFormats.Contains(dP.Ext)) - { - return ContentType.Document; - } - else if (OtherFormats.Contains(dP.Ext)) - { - return ContentType.Program; - } - else if (DAZFormats.Contains(dP.Ext)) - { - return ContentType.DAZ_File; - } - - // The most obvious comment ever - implied else :\ - return ContentType.Unknown; - } - - - public static bool ValidImportExtension(string ext) => AcceptableImportFormats.Contains(ext); - - /// - /// Adds the file name to the tags name. - /// - protected void InitializeTagsList() { - var fileName = IOPath.GetFileName(Path); - var tokens = fileName.Split(' '); - Tags = new List(tokens.Length); - Tags.AddRange(tokens); - } - - public static bool FindFileInDPFiles(string path, out DPFile file) - { - if (DPFiles.TryGetValue(path, out file)) return true; - - file = null; - return false; - } - - } - -} diff --git a/src/DP/DPFolder.cs b/src/DP/DPFolder.cs deleted file mode 100644 index 48089bd..0000000 --- a/src/DP/DPFolder.cs +++ /dev/null @@ -1,267 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE - -using System; -using System.Collections.Generic; -using System.Linq; -using IOPath = System.IO.Path; -namespace DAZ_Installer.DP -{ - internal class DPFolder : DPAbstractFile - { - - internal List subfolders = new List(); - private Dictionary children = new Dictionary(); - internal bool isContentFolder { get; set;} - /// - /// Determined later in ProcessArchive(). - /// - internal bool isPartOfContentFolder - { - get => (Parent?.isPartOfContentFolder ?? false) || (Parent?.isContentFolder ?? false); - } - internal DPFolder(string path, DPFolder parent) : base(path) - { - UID = DPIDManager.GetNewID(); - // Check if path is root. - // GetDirectoryName returns "" if it ends with a slash. - // Ex: D:/folderName/ will return ""; whereas D:/folderName will return "folderName" - Path = PathHelper.GetDirectoryPath(path); - FileName = IOPath.GetFileName(Path); - - //if (relativePathBase != null) - //{ - // relativePath = Path.GetRelativePath(path, relativePathBase); - //} - Parent = parent; - WillExtract = true; - DPProcessor.workingArchive.Folders.TryAdd(Path, this); - - } - - internal static DPFolder CreateFolderForFile(string dpFilePath) - { - var workingStr = DPCommon.Up(dpFilePath); - DPFolder firstFolder = null; - DPFolder previousFolder = null; - - // Continously get relative path. - while (workingStr != "") - { - // to do: - - var found = DPProcessor.workingArchive.RecursivelyFindFolder(workingStr, out _); - - if (!found) - { - if (firstFolder == null) - { - firstFolder = new DPFolder(workingStr, null); - previousFolder = firstFolder; - } - else - { - var workingParent = new DPFolder(workingStr, previousFolder); - //if (previousFolder != null) - //{ - // var IDPFolder = (DPAbstractFile) previousFolder; - // workingParent.addChild(ref IDPFolder); - //} - previousFolder = workingParent; - } - } - workingStr = DPCommon.Up(workingStr); - } - return firstFolder; - } - - internal void UpdateChildrenRelativePaths(DPSettings settings) - { - // Needs to be relative to the content folder. - if (isContentFolder) - { - foreach (var child in children.Values) - { - // This prevents the code for running twice on a child that was previously processed when ManifestAndAuto is on. - if (!string.IsNullOrEmpty(child.RelativePath) && !string.IsNullOrEmpty(child.RelativeTargetPath)) - continue; - child.RelativePath = CalculateChildRelativePath(child); - child.RelativeTargetPath = CalculateChildRelativeTargetPath(child, settings); - } - - } - else - { - var contentFolder = GetContentFolder(); - if (contentFolder != null) - { - foreach (var child in children.Values) - { - // This prevents the code for running twice on a child that was previously processed when ManifestAndAuto is on. - if (!string.IsNullOrEmpty(child.RelativePath) && !string.IsNullOrEmpty(contentFolder.RelativeTargetPath)) - continue; - child.RelativePath = contentFolder.CalculateChildRelativePath(child); - child.RelativeTargetPath = CalculateChildRelativeTargetPath(child, settings); - } - } - } - - } - - /// - /// Calculates the path of a child relative to this folder. - /// - /// The child of this folder. - /// A string representing the relative path of the child relative to this folder. - internal string CalculateChildRelativePath(DPAbstractFile child) => PathHelper.GetRelativePath(child.Path, Path); - - /// - /// Calculates the target path of a child relative to this folder. Requires the settings object to - /// check if the folder name is in . - /// - /// The child of this folder. - /// The settings object in use. - /// A string representing the target path of the child relative to this folder. - internal string CalculateChildRelativeTargetPath(DPAbstractFile child, DPSettings settings) - { - var containsKey = settings.folderRedirects.ContainsKey(FileName); - if (!containsKey || (isContentFolder && !containsKey)) return child.RelativePath; - - var i = Path.LastIndexOf(PathHelper.GetSeperator(Path)); - var newPath = PathHelper.NormalizePath( - i != -1 ? Path.Substring(0, i + 1) + settings.folderRedirects[FileName] : settings.folderRedirects[FileName] - ); - var childNewPath = PathHelper.NormalizePath(child.Path); - childNewPath = childNewPath.Replace(IOPath.GetDirectoryName(childNewPath), newPath); - - return PathHelper.GetRelativePath(childNewPath, newPath); - } - - internal DPFolder GetContentFolder() - { - if (Parent == null) return null; - DPFolder workingFolder = this; - while (workingFolder != null && workingFolder.isContentFolder == false) - { - workingFolder = workingFolder.Parent; - } - return workingFolder; - } - - /// - /// Handles the addition of the file to children property and subfolders property (if child is a DPFolder). - /// - /// DPFolder, DPArchive, DPFile - internal void addChild(DPAbstractFile child) - { - if (child.GetType() == typeof(DPFolder)) - { - var dpFolder = (DPFolder)child; - subfolders.Add(dpFolder); - return; - } - children.TryAdd(child.Path, child); - } - - internal void removeChild(DPAbstractFile child) - { - if (child.GetType() == typeof(DPFolder)) - { - var dpFolder = (DPFolder)child; - subfolders.Remove(dpFolder); - return; - } - children.Remove(child.Path); - } - - internal DPAbstractFile[] GetFiles() - { - return children.Values.ToArray(); - } - - internal DPFolder FindFolder(string _path) - { - if (Path == _path) return this; - else - { - foreach (var folder in subfolders) - { - var result = folder.FindFolder(_path); - if (result != null) return result; - } - } - return null; - } - internal static DPFolder[] FindChildFolders(string _path, DPFolder self) - { - var folderArr = new List(); - foreach (var folder in DPProcessor.workingArchive.Folders.Values) - { - if (folder == self) continue; - // And make sure it only is one level up. - if (folder.Path.Contains(_path) && IOPath.GetFileName(_path) == IOPath.GetFileName(folder.Path) - && PathHelper.GetNumOfLevelsAbove(folder.Path, _path) == 1) - { - folderArr.Add(folder); - } - } - return folderArr.ToArray(); - } - /// - /// - ///

This function removes and updates the root folders list instead of root contents list.

- ///
- /// The new parent for this folder. - - internal override void UpdateParent(DPFolder? newParent) { - // If we were null, but now we're not... - if (_parent == null && newParent != null) { - // Remove ourselves from root folders list of the working archive. - try { - DPProcessor.workingArchive.RootFolders.Remove(this); - } catch {} - - // Call the folder's addChild() to add ourselves to the children list. - newParent.addChild(this); - _parent = newParent; - } else if (_parent == null && newParent == null) { - // Try to find a parent. - var potParent = DPProcessor.workingArchive.FindParent(this); - - // If we found a parent, then update it. This function will be called again. - if (potParent != null) { - Parent = potParent; - } else { - // Otherwise, create a folder for us. - potParent = CreateFolderForFile(Path); - - // If we have successfully created a folder for us, then update it. This function will be called again. - if (potParent != null) Parent = potParent; - else { // Otherwise, we are supposed to be at root. - _parent = null; - if (!DPProcessor.workingArchive.RootFolders.Contains(this)) { - DPProcessor.workingArchive.RootFolders.Add(this); - } - } - } - } else if (_parent != null && newParent != null) { - // Remove ourselves from previous parent children. - _parent.removeChild(this); - - // Add ourselves to new parent's children. - newParent.addChild(this); - - _parent = newParent; - } else if (_parent != null && newParent == null) { - // Remove ourselves from previous parent's children. - _parent.removeChild(this); - - // Add ourselves to the archive's root content list. - DPProcessor.workingArchive.RootFolders.Add(this); - _parent = newParent; - } - } - - - } -} diff --git a/src/DP/DPIDManager.cs b/src/DP/DPIDManager.cs deleted file mode 100644 index 8a9f7b5..0000000 --- a/src/DP/DPIDManager.cs +++ /dev/null @@ -1,17 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE - -namespace DAZ_Installer.DP -{ - internal struct DPIDManager - { - private static uint lastID = 1; - - /// - /// - /// - /// A unique new tag. - internal static uint GetNewID() => lastID++; - - } -} diff --git a/src/DP/DPProcessor.cs b/src/DP/DPProcessor.cs deleted file mode 100644 index dfd336a..0000000 --- a/src/DP/DPProcessor.cs +++ /dev/null @@ -1,443 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE - -using System; -using System.Linq; -using System.Windows.Forms; -using System.IO; -using System.Collections.Generic; - -namespace DAZ_Installer.DP -{ - // GOAL: Extract files through RAR. While it discovers files, add it to list. - // Then, deeply analyze each file; determine best approach; and execute best approach (or ask). - // Lastly, clean up. - internal static class DPProcessor - { - // SecureString - System.Security - // We use these variables in case the user changes the settings in mist of an extraction process - public static DPSettings settingsToUse = DPSettings.currentSettingsObject; - public static string TempLocation => Path.Combine(settingsToUse.tempPath, @"DazProductInstaller\"); - public static string DestinationPath => settingsToUse.destinationPath; - public static DPAbstractArchive workingArchive; - public static HashSet previouslyInstalledArchiveNames { get; private set; } = new HashSet(); - public static List doNotProcessList { get; } = new List(); - public static uint workingArchiveFileCount { get; set; } = 0; // can disgard. - public static SettingOptions OverwriteFiles => settingsToUse.OverwriteFiles; - - static DPProcessor() => DPDatabase.GetInstalledArchiveNamesQ(UpdateInstalledArchiveNames); - - public static DPAbstractArchive ProcessInnerArchive(DPAbstractArchive archiveFile) - { - workingArchive = archiveFile; - DPFile.DPFiles.Clear(); - try - { - Directory.CreateDirectory(TempLocation); - } - catch (Exception e) { DPCommon.WriteToLog($"Unable to create temp directory. {e}"); } - if (previouslyInstalledArchiveNames.Contains(Path.GetFileName(archiveFile.FileName))) - { - // ,_, - switch (settingsToUse.installPrevProducts) - { - case SettingOptions.No: - return null; - case SettingOptions.Prompt: - var result = MessageBox.Show($"It seems that \"{archiveFile.FileName}\" was already processed. " + - $"Do you wish to continue processing this file?", "Archive already processed", MessageBoxButtons.YesNo, MessageBoxIcon.Information); - if (result == DialogResult.No) return null; - break; - } - } - try - { - archiveFile.Peek(); - } catch (Exception ex) - { - archiveFile.errored = true; - HandleEarlyExit(archiveFile); - DPCommon.WriteToLog($"Unable to peek into inner archive: {Path.GetFileName(archiveFile.Path)}." + - $"REASON: {ex}"); - } - // TO DO: Highlight files in red for files that failed to extract. - Extract.ExtractPage.AddToList(archiveFile); - Extract.ExtractPage.AddToHierachy(archiveFile); - // Check if we have enough room. - if (!DestinationHasEnoughSpace()) - { - // TODO: Warn user that there was not enough space and cancel. - DPCommon.WriteToLog("Destination did not have enough space. Operation aborted."); - HandleEarlyExit(archiveFile); - archiveFile.errored = true; - return archiveFile; - } - - archiveFile.ManifestFile = archiveFile.FindFileViaNameContains("Manifest.dsx") as DPDSXFile; // null if not found - - try - { - PrepareOperations(archiveFile); - DetermineContentFolders(archiveFile); - UpdateRelativePaths(archiveFile); - DetermineFilesToExtract(archiveFile); - } catch (Exception ex) - { - archiveFile.errored = true; - HandleEarlyExit(archiveFile); - DPCommon.WriteToLog($"Failed to prepare for extraction for {archiveFile.FileName} (inner archive). REASON: {ex}"); - } - try - { - archiveFile.Extract(); - } catch (Exception ex) - { - archiveFile.errored = true; - HandleEarlyExit(archiveFile); - DPCommon.WriteToLog($"Failed to extract files for {archiveFile.FileName} (inner archive). REASON: {ex}"); - } - - DPCommon.WriteToLog("We are done"); - - archiveFile.ProgressCombo?.Remove(); - - var analyzeCombo = new DPProgressCombo(); - analyzeCombo.ChangeProgressBarStyle(true); - analyzeCombo.UpdateText("Analyzing file contents..."); - archiveFile.Type = archiveFile.DetermineArchiveType(); - DPCommon.WriteToLog("Analyzing files..."); - analyzeCombo.UpdateText("Creating library item..."); - try { - archiveFile.GetTags(); - } catch { DPCommon.WriteToLog("Failed to get tags."); } - analyzeCombo?.Remove(); - - for (var i = 0; i < archiveFile.InternalArchives.Count; i++) - { - var arc = archiveFile.InternalArchives[i]; - if (arc.WasExtracted) ProcessInnerArchive(arc); - } - - // Create record. - var record = archiveFile.CreateRecords(); - if (record != null) previouslyInstalledArchiveNames.Add(archiveFile.FileName); - archiveFile.ReleaseArchiveHandles(); - // TO DO: Only add if successful extraction, and all files from temp were moved, and/or user didn't cancel operation. - DPCommon.WriteToLog($"Archive Type: {archiveFile.Type}"); - return archiveFile; - } - - public static DPAbstractArchive? ProcessArchive(string filePath, DPSettings settings) - { - settingsToUse = settings; - DPFile.DPFiles.Clear(); - try - { - Directory.CreateDirectory(TempLocation); - } - catch (Exception e) { DPCommon.WriteToLog($"Unable to create directory. {e}"); } - if (previouslyInstalledArchiveNames.Contains(Path.GetFileName(Path.GetFileName(filePath)))) - { - // ,_, - switch (settingsToUse.installPrevProducts) - { - case SettingOptions.No: - return null; - case SettingOptions.Prompt: - var result = MessageBox.Show($"It seems that \"{Path.GetFileName(filePath)}\" was already processed. Do you wish to continue processing this file?", "Archive already processed", MessageBoxButtons.YesNo, MessageBoxIcon.Information); - if (result == DialogResult.No) return null; - break; - } - } - // Create new archive. - var archiveFile = DPAbstractArchive.CreateNewArchive(filePath, false); - if (archiveFile == null) return null; - workingArchive = archiveFile; - try - { - archiveFile.Peek(); - } - catch (Exception ex) - { - archiveFile.errored = true; - HandleEarlyExit(archiveFile); - DPCommon.WriteToLog($"Unable to peek into inner archive: {Path.GetFileName(archiveFile.Path)}." + - $"REASON: {ex}"); - } - // TO DO: Highlight files in red for files that failed to extract. - Extract.ExtractPage.AddToList(archiveFile); - Extract.ExtractPage.AddToHierachy(archiveFile); - - // Check if we have enough room. - if (!DestinationHasEnoughSpace()) - { - // TODO: Warn user that there was not enough space and cancel. - DPCommon.WriteToLog("Destination did not have enough space. Operation aborted."); - archiveFile.errored = true; - HandleEarlyExit(archiveFile); - return archiveFile; - } - - archiveFile.ManifestFile = archiveFile.FindFileViaNameContains("Manifest.dsx") as DPDSXFile; - try - { - PrepareOperations(archiveFile); - DetermineContentFolders(archiveFile); - UpdateRelativePaths(archiveFile); - DetermineFilesToExtract(archiveFile); - } catch (Exception ex) - { - archiveFile.errored = true; - HandleEarlyExit(archiveFile); - DPCommon.WriteToLog($"Failed to prepare for extraction for {archiveFile.FileName}. REASON: {ex}"); - } - // TODO: Ensure that archive progress combo is not null. - try - { - archiveFile.Extract(); - } catch (Exception ex) - { - archiveFile.errored = true; - HandleEarlyExit(archiveFile); - DPCommon.WriteToLog($"Failed to extract files for {archiveFile.FileName}. REASON: {ex}"); - } - DPCommon.WriteToLog("We are done"); - - archiveFile.ProgressCombo?.Remove(); - - var analyzeCombo = new DPProgressCombo(); - analyzeCombo.ChangeProgressBarStyle(true); - analyzeCombo.UpdateText("Analyzing file contents..."); - archiveFile.Type = archiveFile.DetermineArchiveType(); - DPCommon.WriteToLog("Analyzing files..."); - analyzeCombo.UpdateText("Creating library item..."); - try { - archiveFile.GetTags(); - } catch { DPCommon.WriteToLog("Failed to get tags."); } - analyzeCombo?.Remove(); - for (var i = 0; i < archiveFile.InternalArchives.Count; i++) - { - var arc = archiveFile.InternalArchives[i]; - if (arc.WasExtracted) ProcessInnerArchive(arc); - } - - DPCommon.WriteToLog($"Archive Type: {archiveFile.Type}"); - // Create records and save it to disk. - // TODO: Add a flag to make sure records aren't created for completely - // failed archives (such as an "zip" archive when really it's a jpg file). - var record = archiveFile.CreateRecords(); - if (record != null) previouslyInstalledArchiveNames.Add(archiveFile.FileName); - archiveFile.ReleaseArchiveHandles(); - - return archiveFile; - } - - public static void UpdateInstalledArchiveNames(HashSet strings) => previouslyInstalledArchiveNames = strings; - - - private static void UpdateRelativePaths(DPAbstractArchive archive) - { - foreach (var content in archive.RootContents) - content.RelativePath = content.RelativeTargetPath = content.Path; - foreach (var folder in archive.Folders.Values) - folder.UpdateChildrenRelativePaths(settingsToUse); - } - - public static void DetermineFilesToExtract(DPAbstractArchive archive) - { - // Handle Manifest first. - if (archive.ManifestFile != null && archive.ManifestFile.WasExtracted) - { - if (settingsToUse.handleInstallation == InstallOptions.ManifestAndAuto || - settingsToUse.handleInstallation == InstallOptions.ManifestOnly) - { - var manifest = archive.ManifestFile; - var manifestDestinations = manifest.GetManifestDestinations(); - - foreach (var file in archive.Contents) - { - if (manifestDestinations.ContainsKey(file.Path)) - { - try - { - file.TargetPath = GetTargetPath(file, overridePath: manifestDestinations[file.Path]); - file.WillExtract = true; - // TO DO: Add directories if does not exist. - Directory.CreateDirectory(Path.GetDirectoryName(file.TargetPath)); - } - catch (Exception ex) { - DPCommon.WriteToLog($"An error occured while attempting to create directory. REASON: {ex}"); - file.errored = true; - } - } - else - { - file.WillExtract = settingsToUse.handleInstallation != InstallOptions.ManifestOnly; - } - } - } - - } - if (settingsToUse.handleInstallation == InstallOptions.Automatic || settingsToUse.handleInstallation == InstallOptions.ManifestAndAuto) - { - // Get contents where file was not extracted. - var folders = archive.Folders.Values.ToArray(); - foreach (var folder in folders) - { - // TO DO: Check if folder is a subfolder of a folder that is a content folder. - if (folder.isContentFolder || folder.isPartOfContentFolder) - { - // Update children's relative path. - folder.UpdateChildrenRelativePaths(settingsToUse); - - foreach (var child in folder.GetFiles()) - { - // Get destination path. - var dPath = GetTargetPath(child); - // Update child destination path. - child.TargetPath = dPath; - child.WillExtract = true; - } - } - } - // Now hunt down all files in folders that aren't in content folders. - foreach (var folder in folders) - { - if (folder.isContentFolder) continue; - // Add all archives to the inner archives to process for later processing. - foreach (var file in folder.GetFiles()) - { - if (file is DPAbstractArchive) - { - var arc = (DPAbstractArchive)file; - arc.WillExtract = true; - arc.TargetPath = GetTargetPath(arc, true); - // Add to queue. - workingArchive.InternalArchives.Add(arc); - } - } - } - - // Hunt down all files in root content. - - foreach (var content in archive.RootContents) - { - if (content is DPAbstractArchive) - { - var arc = (DPAbstractArchive)content; - arc.WillExtract = true; - arc.TargetPath = GetTargetPath(arc, true); - // Add to queue. - workingArchive.InternalArchives.Add(arc); - } - } - } - - } - - /// - /// This function returns the target path based on whether it is saving to it's destination or to a - /// temporary location, whether the has a relative path or not, and whether - /// the file's parent is in folderRedirects. - /// Additionally, there is which will be used for combining paths internally; - /// however, this will be ignored if the parent name is in the user's folder redirects. - /// - /// The file to get a target path for. - /// Determines whether to get a target path saving to a temporary location. - /// The path to combine with instead of usual combining. - /// The target path for the specified file. - private static string GetTargetPath(DPAbstractFile file, bool saveToTemp = false, string overridePath = null) - { - var filePathPart = !string.IsNullOrEmpty(overridePath) ? overridePath : file.RelativeTargetPath; - - if (file.Parent is null || !settingsToUse.folderRedirects.ContainsKey(Path.GetFileName(file.Parent.Path))) - return Path.Combine(saveToTemp ? TempLocation : DestinationPath, filePathPart); - - return Path.Combine(saveToTemp ? TempLocation : DestinationPath, - file.RelativeTargetPath ?? file.Parent.CalculateChildRelativeTargetPath(file, settingsToUse)); - } - // TODO: Handle situations where the destination no longer exists or has no access. - private static bool DestinationHasEnoughSpace() { - var destinationDrive = new DriveInfo(Path.GetPathRoot(DestinationPath)); - return (ulong) destinationDrive.AvailableFreeSpace > workingArchive.TrueArchiveSize; - } - // TODO: Handle situations where the destination no longer exists or has no access. - private static bool TempHasEnoughSpace() { - var tempDrive = new DriveInfo(Path.GetPathRoot(TempLocation)); - return (ulong) tempDrive.AvailableFreeSpace > workingArchive.TrueArchiveSize; - } - - private static void DetermineContentFolders(DPAbstractArchive archiveFile) - { - // A content folder is a folder whose name is contained in the user's common content folders list - // or in their folder redirects map. - - - // Prepare sort so that the first elements in folders are the ones at root. - var folders = archiveFile.Folders.Values.ToArray(); - var foldersKeys = new byte[folders.Length]; - - for (int i = 0; i < foldersKeys.Length; i++) - { - foldersKeys[i] = PathHelper.GetNumOfLevels(folders[i].Path); - } - - // Elements at the beginning are folders at root levels. - Array.Sort(foldersKeys, folders); - - foreach (var folder in folders) - { - var folderName = Path.GetFileName(folder.Path); - var elgibleForContentFolderStatus = settingsToUse.commonContentFolderNames.Contains(folderName) || - settingsToUse.folderRedirects.ContainsKey(folderName); - if (folder.Parent is null) - folder.isContentFolder = elgibleForContentFolderStatus; - else - { - if (folder.Parent.isContentFolder || folder.Parent.isPartOfContentFolder) continue; - folder.isContentFolder = elgibleForContentFolderStatus; - } - } - } - - - // TODO: Clear temp needs to remove as much space as possible. It will error when we have file handles. - internal static void ClearTemp() { - try { - // Note: UnauthorizedAccess is called when a file has the read-only attribute. - // TODO: Async call to change file attributes and delete them. - if (Directory.Exists(TempLocation)) { - Directory.Delete(TempLocation, true); - DPCommon.WriteToLog("Deleted temp files"); - } - } catch {} - } - - private static void PrepareOperations(DPAbstractArchive archive) { - - if (!archive.CanReadWithoutExtracting) { - if (!TempHasEnoughSpace()) { - ClearTemp(); - if (!TempHasEnoughSpace()) { - DPCommon.WriteToLog("Temp location does not have enough space. Operation aborted."); - return; - } else { - workingArchive.ReadMetaFiles(); - } - } - } else { - workingArchive.ReadMetaFiles(); - } - } - - private static void HandleEarlyExit(DPAbstractArchive archive) - { - archive.ProgressCombo?.Remove(); - try - { - archive.ReleaseArchiveHandles(); - } - catch { } - } - } -} diff --git a/src/DP/DPProductInfo.cs b/src/DP/DPProductInfo.cs deleted file mode 100644 index 51c69b8..0000000 --- a/src/DP/DPProductInfo.cs +++ /dev/null @@ -1,21 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE -using System.Collections.Generic; - -namespace DAZ_Installer.DP { - internal struct DPProductInfo { - internal string ProductName = string.Empty; - internal HashSet? Tags = null; - internal HashSet Authors = new HashSet(1); - internal string SKU = string.Empty; - - public DPProductInfo() {} - - internal DPProductInfo(string productName, HashSet author = null, string? sku = null, HashSet? tags = null) { - if (tags != null) Tags = tags; - if (author != null) Authors = author; - if (sku != null) SKU = sku; - ProductName = productName; - } - } -} \ No newline at end of file diff --git a/src/DP/DPProgressCombo.cs b/src/DP/DPProgressCombo.cs deleted file mode 100644 index f031297..0000000 --- a/src/DP/DPProgressCombo.cs +++ /dev/null @@ -1,93 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE -using System.Windows.Forms; -using System.Drawing; -using System.Collections.Generic; - -namespace DAZ_Installer.DP { - internal class DPProgressCombo { - internal static Stack ProgressCombos = new Stack(3); - internal TableLayoutPanel Panel {get; private set; } - internal Label ProgressBarLbl { get; private set; } - internal ProgressBar ProgressBar { get; private set; } - - internal bool IsMarqueueProgressBar { get => ProgressBar.Style == ProgressBarStyle.Marquee; } - - internal DPProgressCombo() { - Extract.ExtractPage.Invoke(CreateProgressCombo); - Extract.ExtractPage.Invoke(Extract.ExtractPage.AddNewProgressCombo, this); - ProgressCombos.Push(this); - } - - private void CreateProgressCombo() { - - // Panel - Panel = new TableLayoutPanel(); - Panel.Dock = DockStyle.Fill; - Panel.ColumnCount = 1; - Panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); - Panel.RowCount = 2; - Panel.RowStyles.Add(new ColumnStyle(SizeType.AutoSize)); - Panel.RowStyles.Add(new ColumnStyle(SizeType.AutoSize)); - - ProgressBarLbl = new Label(); - ProgressBarLbl.Text = "Processing ..."; - ProgressBarLbl.Dock = DockStyle.Fill; - ProgressBarLbl.AutoEllipsis = true; - ProgressBarLbl.TextAlign = ContentAlignment.BottomLeft; - ProgressBarLbl.MinimumSize = new Size(0, 25); - Panel.Controls.Add(ProgressBarLbl, 0, 0); - - ProgressBar = new ProgressBar(); - ProgressBar.Value = 50; - ProgressBar.Dock = DockStyle.Fill; - ProgressBar.MinimumSize = new Size(0, 18); - ProgressBar.MarqueeAnimationSpeed /= 5; - Panel.Controls.Add(ProgressBar, 0, 1); - - // ProgressBar.CheckForIllegalCrossThreadCalls = false; - } - - - internal void ChangeProgressBarStyle(bool marqueue) { - if (IsMarqueueProgressBar == marqueue) return; - if (Extract.ExtractPage.InvokeRequired) { - Extract.ExtractPage.Invoke(ChangeProgressBarStyle, marqueue); - return; - } - - ProgressBar.SuspendLayout(); - if (marqueue) { - ProgressBar.Value = 10; - ProgressBar.Style = ProgressBarStyle.Marquee; - } else { - ProgressBar.Value = 50; - ProgressBar.Style = ProgressBarStyle.Blocks; - } - ProgressBar.ResumeLayout(); - - } - - internal static void RemoveAll() { - Extract.ExtractPage.Invoke(Extract.ExtractPage.ResetExtractPage); - ProgressCombos.Clear(); - } - - internal void Remove() { - if (ProgressCombos.TryPop(out _)) - Extract.ExtractPage.DeleteProgressionCombo(this); - } - - internal void UpdateText(string text) { - ProgressBarLbl.Text = - Extract.ExtractPage.mainProcLbl.Text = - text; - } - - ~DPProgressCombo() { - ProgressBar.Dispose(); - ProgressBarLbl.Dispose(); - Panel.Dispose(); - } - } -} \ No newline at end of file diff --git a/src/DP/DPRARArchive.cs b/src/DP/DPRARArchive.cs deleted file mode 100644 index e2a4547..0000000 --- a/src/DP/DPRARArchive.cs +++ /dev/null @@ -1,404 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE - -using System; -using System.Text; -using System.Collections.Generic; -using System.Windows.Forms; -using DAZ_Installer.External; -using IOPath = System.IO.Path; -using System.IO; - -namespace DAZ_Installer.DP -{ - // ZIP Transversal Check - /* - destFileName = Path.GetFullPath(Path.Combine(destDirectory, entry.Key)); - string fullDestDirPath = Path.GetFullPath(destDirectory + Path.DirectorySeparatorChar); - if (!destFileName.StartsWith(fullDestDirPath)) { - throw new ExtractionException("Entry is outside of the target dir: " + destFileName); - } - */ - // TO DO: Add tag property. - - - internal class DPRARArchive : DPAbstractArchive - { - public bool passwordFailed = false; - public bool cancelledOperation = false; - public bool secondPasswordPromptHasSeen = false; - private List lastVolumes { get; } = new List(); - private Dictionary volumePairs = new Dictionary(); // First key is the OLD nonworking one, Second key is the working one. - - internal override bool CanReadWithoutExtracting { get => false; } - protected char[] password; - - protected char internalDictSeperator = '\\'; - - public DPRARArchive(string _path, bool innerArchive = false, string? relativePathBase = null) : base(_path, innerArchive, relativePathBase) - { - - } - #region Event Methods - - public void HandleProgression(RAR sender, ExtractionProgressEventArgs e) { - var progress = (int)Math.Floor(e.PercentComplete); - ProgressCombo.UpdateText($"Extracting {e.FileName}...({progress}%)"); - ProgressCombo.ProgressBar.Value = progress; - - } - - // Will probably remove for safety. - public void HandlePasswordProtected(RAR sender, PasswordRequiredEventArgs e) { - var passDlg = new PasswordInput(); - passDlg.archiveName = IOPath.GetFileName(sender.ArchivePathName); - if (sender.CurrentFile == null) { - passDlg.message = $"{IOPath.GetFileName(Path)} is encrypted. Please enter password to decrypt archive."; - } else { - if ((sender.arcData.Flags & 0x0080) != 0) { - passDlg.message = "Password was incorrect. Please re-enter password to decrypt archive."; - } - } - passDlg.ShowDialog(); - - if (passDlg.password != null) { - e.Password = passDlg.password; - e.ContinueOperation = true; - } else { - e.ContinueOperation = false; - cancelledOperation = true; - } - } - - public void RARHandlePassword(RAR sender, PasswordRequiredEventArgs e) - { - var password = GetPassword(); - if (string.IsNullOrEmpty(password)) - { - HandlePasswordProtected(sender, e); - return; - } - e.Password = GetPassword(); - e.ContinueOperation = !cancelledOperation; - secondPasswordPromptHasSeen = true; - } - - public void RARHandleVolumes(RAR _, MissingVolumeEventArgs e) - { - e.VolumeName = GetRightVolume(e.VolumeName); - } - - public void HandleNewVolume(RAR sender, NewVolumeEventArgs e) - { - if (sender.ArchivePathName != e.VolumeName) ConnectVolumeDir(e.VolumeName); - if (DPExtractJob.workingJob.doNotProcess.Contains(IOPath.GetFileName(sender.ArchivePathName))) - { - DPExtractJob.workingJob.doNotProcess.Add(IOPath.GetFileName(sender.ArchivePathName)); - } - } - - private void HandleMissingVolume(RAR sender, MissingVolumeEventArgs e) - { - var result = MessageBox.Show($"{sender.CurrentFile.FileName} is missing volume : {e.VolumeName}. Do you know where this file is? ", - "Missing volume", MessageBoxButtons.YesNo); - if (result == DialogResult.Yes) { - string fileName = MainForm.activeForm.ShowFileDialog("RAR files (*.rar)|*.rar", "rar"); - if (fileName != null) - { - e.VolumeName = fileName; - e.ContinueOperation = true; - return; - } - } - e.ContinueOperation = false; - } - - - public void HandleNewFile(RAR sender, NewFileEventArgs e) - { - DPCommon.WriteToLog(e.fileInfo.FileName); - if (e.fileInfo.IsDirectory) - { - if (!FolderExists(e.fileInfo.FileName)) - { - _ = new DPFolder(e.fileInfo.FileName, null); - //newDir.parent = workingArchive.FindParent(ref IDP); - //if (newDir.parent == null) workingArchive.rootFolders.Add(newDir); - } - - } - else - { - if (DPFile.ValidImportExtension(GetExtension(e.fileInfo.FileName))) - { - // File is archive. - var newArchive = CreateNewArchive(e.fileInfo.FileName, true, null); - newArchive.ParentArchive = this; - } - else - { - var newFile = DPFile.CreateNewFile(e.fileInfo.FileName, null); - newFile.AssociatedArchive = this; - } - } - } - - - public void ConnectVolumeDir(string dirPath) - { - lastVolumes.Add(IOPath.GetFileName(dirPath)); - HierachyName = FileName + " ("; - foreach (var volume in lastVolumes) - { - HierachyName += $"{volume}/"; - } - HierachyName = HierachyName.TrimEnd('/') + ')'; - } - - public void SetPassword(string pass) - { - password = pass.ToCharArray(); - } - - public string GetPassword() - { - return new string(password); - } - - public void AddVolumePair(string expectedVolume, string rightVolume) - { - volumePairs.Add(expectedVolume, rightVolume); - } - - public string GetRightVolume(string expectedVolume) - { - if (volumePairs.TryGetValue(expectedVolume, out string rightVolume)) - { - return rightVolume; - } - return null; - } - #endregion - #region Override Methods - - internal override void ReadContentFiles() - { - // At this point, the files should have been extracted. - foreach (var file in DazFiles) { - // We only want daz files that successfully extracted. - if (!file.WasExtracted) continue; - try - { - using (var reader = new StreamReader(file.ExtractedPath, Encoding.UTF8, true)) - { - file.ReadContents(reader); - } - } - catch (Exception ex) - { - DPCommon.WriteToLog($"An unexpected error occured in ReadContentFiles(). REASON: {ex}"); - } - } - } - - internal override void ReadMetaFiles() - { - try - { - using (RAR handler = new RAR(IsInnerArchive ? ExtractedPath : Path)) - { - handler.Open(); - var extractedFileCount = 0; - foreach (var file in DSXFiles) - { - if (file.WasExtracted) - { - file.CheckContents(); - extractedFileCount++; - } - } - if (extractedFileCount == DSXFiles.Count) return; - - while (handler.ReadHeader()) - { - var dsxfile = DSXFiles.Find(f => f.Path == handler.CurrentFile.FileName); - if (dsxfile is null) continue; - ExtractFile(handler); - if (dsxfile.WasExtracted) - dsxfile.CheckContents(); - } - } - } catch (Exception ex) - { - DPCommon.WriteToLog($"An unexpected error occured in ReadMetaFiles(). REASON: {ex}"); - } - } - internal override void Extract() { - mode = Mode.Extract; - ProgressCombo ??= new DPProgressCombo(); - using (var RARHandler = new RAR(IsInnerArchive ? ExtractedPath : Path)) { - RARHandler.PasswordRequired += HandlePasswordProtected; - RARHandler.ExtractionProgress += HandleProgression; - try { - // TODO: Update destination path. - RARHandler.Open(RAR.OpenMode.Extract); - var flags = (RAR.ArchiveFlags) RARHandler.arcData.Flags; - var isFirstVolume = flags.HasFlag(RAR.ArchiveFlags.FirstVolume); - var isVolume = flags.HasFlag(RAR.ArchiveFlags.Volume); - - if (isVolume && !isFirstVolume) { - MessageBox.Show("Archive is not the first volume. Archive will not be processed.", "Cannot process second volume", MessageBoxButtons.OK); - throw new IOException("Archive wasn't first volumne."); - } - - while (RARHandler.ReadHeader()) { - if (ExtractFile(RARHandler)) { - // TODO: Something - } else - { - RARHandler.Skip(); - } - } - - RARHandler.Close(); - } catch (Exception e) { - DPCommon.WriteToLog($"An unexpected error occured while processing for RAR Archive. REASON: {e}"); - } - } - ProgressCombo?.Remove(); - } - internal override void Peek() - { - mode = Mode.Peek; - using (var RARHandler = new RAR(IsInnerArchive ? ExtractedPath : Path)) { - RARHandler.PasswordRequired += HandlePasswordProtected; - RARHandler.MissingVolume += HandleMissingVolume; - RARHandler.ExtractionProgress += HandleProgression; - RARHandler.NewFile += HandleNewFile; - - try { - RARHandler.DestinationPath = IOPath.Combine(DPProcessor.TempLocation, IOPath.GetFileNameWithoutExtension(Path)); - - // Create path and see if it exists. - Directory.CreateDirectory(RARHandler.DestinationPath); - - RARHandler.Open(RAR.OpenMode.List); - var flags = (RAR.ArchiveFlags) RARHandler.arcData.Flags; - var isFirstVolume = flags.HasFlag(RAR.ArchiveFlags.FirstVolume); - var isVolume = flags.HasFlag(RAR.ArchiveFlags.Volume); - - if (isVolume && !isFirstVolume) { - MessageBox.Show("Archive is not the first volume. Archive will not be processed.", "Cannot process second volume", MessageBoxButtons.OK); - throw new IOException("Archive wasn't first volumne."); - } - - while (RARHandler.ReadHeader()) { - if (TestFile(RARHandler)) { - // TODO: Something. - } - } - - RARHandler.Close(); - } catch (Exception e) { - errored = true; - DPCommon.WriteToLog($"An unexpected error occured while processing for RAR Archive. REASON: {e}"); - } - } - } - - internal override void ReleaseArchiveHandles() { } - #endregion - - private bool ExtractFile(RAR handler) { - string fileName = handler.CurrentFile.FileName; - bool changedAttributes = false; - EXTRACT: - DPAbstractFile file = null; - try { - if (DPFile.FindFileInDPFiles(fileName, out DPFile file1)) file = file1; - if (file == null) file = Contents.Find(a => a.Path == fileName); - if (file != null && file.WillExtract) - { - handler.DestinationPath = IOPath.GetDirectoryName(file.TargetPath); - // Create folders for the destination path if needed. - try - { - Directory.CreateDirectory(handler.DestinationPath); - } - catch { } - handler.Extract(file.TargetPath); - - // Only update if we didn't error. - file.ExtractedPath = file.TargetPath; - file.WasExtracted = true; - } - else return false; - } catch (IOException e) { - // We errored :(. - if (file != null) file.errored = true; - if (e.Message == "File CRC Error" || e.Message == "File could not be opened.") { - var flags = (RAR.ArchiveFlags) handler.arcData.Flags; - var isVolume = flags.HasFlag(RAR.ArchiveFlags.Volume); - var continuesNext = handler.CurrentFile.ContinuedOnNext; - var isEncrypted = handler.CurrentFile.encrypted; - - if ((!isVolume || !continuesNext) && !isEncrypted) - { - DPCommon.WriteToLog("File CRC error."); - } else { - DPCommon.WriteToLog("An unexpected error occured when extracting a rar file."); - } - - } - // Check to see if we are attempting to overwrite a file that we don't have access to (ex: hidden/read-only/anti-virus/user no access). - if (e.Message == "File write error." || e.Message == "File read error." || e.Message == "File could not be opened.") - { - try - { - FileInfo fileInfo = new FileInfo(file.TargetPath); - if (fileInfo.Exists && !changedAttributes) - { - fileInfo.Attributes = FileAttributes.Normal; - changedAttributes = true; - goto EXTRACT; - } - else - DPCommon.WriteToLog($"Failed to extract file even after file attribute change for {fileName}."); - } catch (Exception ex) - { - DPCommon.WriteToLog($"Unable to extract file and change file attributes for {fileName}. REASON: {ex}"); - } - } - return false; - } - return true; - } - - private bool TestFile(RAR handler) { - try { - // I'm not sure if UnpackedSize returns negative if the file is partial. - TrueArchiveSize += (ulong) Math.Max((long) 0, handler.CurrentFile.UnpackedSize); - handler.Test(); - } catch (IOException e) { - if (e.Message == "File CRC Error" || e.Message == "File could not be opened.") { - var flags = (RAR.ArchiveFlags) handler.arcData.Flags; - var isVolume = flags.HasFlag(RAR.ArchiveFlags.Volume); - var continuesNext = handler.CurrentFile.ContinuedOnNext; - var isEncrypted = handler.CurrentFile.encrypted; - - if ((!isVolume || !continuesNext) && !isEncrypted) - { - return false; - // TODO: Call error tab to handle this matter. - throw new FileFormatException("File CRC error."); - } else { - return false; - // TODO: Call error tab to handle this matter. - throw new FileFormatException("Another error occurred."); - } - } - } - return true; - } - } -} diff --git a/src/DP/DPRegistry.cs b/src/DP/DPRegistry.cs deleted file mode 100644 index 318c08f..0000000 --- a/src/DP/DPRegistry.cs +++ /dev/null @@ -1,55 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Microsoft.Win32; - -namespace DAZ_Installer.DP -{ - /// - /// This class is used to gather important registry values. - /// - internal static class DPRegistry - { - internal static string[] ContentDirectories { get; set; } - internal static string DazAppPath { get; set; } = ""; - internal static bool foundRegistry = false; - internal static bool initalized = false; - - static DPRegistry() - { - var DazStudioKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\DAZ\Studio4"); - if (DazStudioKey != null) - { - ContentDirectories = GetContentDirectories(DazStudioKey); - - // Get App Path. - var valueNames = DazStudioKey.GetValueNames(); - string installPathName = "InstallPath-64"; - foreach (var name in valueNames) - { - if (name.Contains("InstallPath")) installPathName = name; - } - DazAppPath = DazStudioKey.GetValue(installPathName, "") as string; - } - } - - private static string[] GetContentDirectories(RegistryKey key) - { - var dirs = new List(); - byte i = 0; - while (i < byte.MaxValue) - { - var contentDirName = "ContentDir" + i.ToString(); - string contentDirVal = key.GetValue(contentDirName, "") as string; - if (string.IsNullOrEmpty(contentDirVal)) break; - dirs.Add(contentDirVal); - i++; - } - return dirs.ToArray(); - } - } -} diff --git a/src/DP/DPSettings.cs b/src/DP/DPSettings.cs deleted file mode 100644 index 6faeb68..0000000 --- a/src/DP/DPSettings.cs +++ /dev/null @@ -1,200 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE - -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.ConstrainedExecution; -using System.Text; -using System.Windows.Forms; -using Microsoft.VisualBasic.FileIO; -using Newtonsoft.Json; - -namespace DAZ_Installer.DP -{ - public enum SettingOptions - { - No, Yes, Prompt - } - - /// - /// Options for user to handle how to handle extraction (ie: manifest only, etc) - /// - public enum InstallOptions - { - ManifestOnly, ManifestAndAuto, Automatic - } - - public class DPSettings - { - // Note: The JSONSerializer will not use the setter for HashSet, therefore the - [JsonIgnore] - public static DPSettings currentSettingsObject; - public string destinationPath { get; set; } // todo : Ask for daz content directory if no detected daz content paths found. - // TO DO: Use HashSet instead of list. - public string[] detectedDazContentPaths; - public SettingOptions downloadImages { get; set; } = SettingOptions.Prompt; - public string thumbnailsPath { get; set; } = "Thumbnails"; - public InstallOptions handleInstallation { get; set; } = InstallOptions.ManifestAndAuto; - [JsonIgnore] - public static HashSet inititalCommonContentFolderNames { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) { "aniBlocks", "Animals", "Architecture", "Camera Presets", "data", "DAZ Studio Tutorials", "Documentation", "Documents", "Environments", "General", "Light Presets", "Lights", "People", "Presets", "Props", "Render Presets", "Render Settings", "Runtime", "Scene Builder", "Scene Subsets", "Scenes", "Scripts", "Shader Presets", "Shaders", "Support", "Templates", "Textures", "Vehicles" }; - public HashSet commonContentFolderNames { get; set; } = new HashSet(inititalCommonContentFolderNames, StringComparer.OrdinalIgnoreCase); - public Dictionary folderRedirects { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "docs", "Documentation" }, { "Documents", "Documentation" } }; - public string tempPath { get; set; } = Path.Combine(Path.GetTempPath(), "DazProductInstaller"); // - public uint maxTagsToShow { get; set; } = 8; // Keep low because GDI+ slow. - public SettingOptions permDeleteSource { get; set; } = SettingOptions.Prompt; - public SettingOptions installPrevProducts { get; set; } = SettingOptions.Prompt; - public SettingOptions OverwriteFiles { get; set; } = SettingOptions.Yes; - public RecycleOption DeleteAction { get; set; } = RecycleOption.DeletePermanently; - public static string databasePath { get; set; } = "Database"; - [JsonIgnore] - public static bool initalized { get; set; } = false; - [JsonIgnore] - public static bool invalidSettings = false; - private const string SETTINGS_PATH = "settings.json"; - - static DPSettings() - { - currentSettingsObject = new DPSettings(); - currentSettingsObject.Initalize(); - } - - public DPSettings() { } - - public static DPSettings GetCopy() - { - var settings = (DPSettings)currentSettingsObject.MemberwiseClone(); - settings.commonContentFolderNames = new HashSet(currentSettingsObject.commonContentFolderNames, StringComparer.OrdinalIgnoreCase); - settings.folderRedirects = new Dictionary(currentSettingsObject.folderRedirects, StringComparer.OrdinalIgnoreCase); - settings.detectedDazContentPaths = (string[]) currentSettingsObject.detectedDazContentPaths.Clone(); - return settings; - } - - // TODO: Handle situation where new settings were added; ex, OverWriteFiles - public void Initalize() - { - if (initalized) return; - var exists = File.Exists(SETTINGS_PATH); - if (!exists) - { - currentSettingsObject.FinishInitialization(); - return; - } - - var settingsObj = ParseSettings(File.ReadAllText(SETTINGS_PATH)); - if (settingsObj == null) - MessageBox.Show("There was an error processing settings. Settings have been reset to default values.", - "Failed to process settings", MessageBoxButtons.OK, MessageBoxIcon.Error); - else - currentSettingsObject = settingsObj; - currentSettingsObject.FinishInitialization(); - - } - - public void FinishInitialization() - { - ValidateDirectoryPaths(); - detectedDazContentPaths = DPRegistry.ContentDirectories; - DPProcessor.ClearTemp(); - initalized = true; - } - - public DPSettings? ParseSettings(string str) - { - try - { - return JsonConvert.DeserializeObject(str); - } catch (Exception ex) - { - DPCommon.WriteToLog($"Failed to parse settings. REASON: {ex}"); - } - return null; - } - - private void ValidateDirectoryPaths() - { - bool destExists = !string.IsNullOrEmpty(destinationPath) && Directory.Exists(destinationPath); - bool thumbExists = !string.IsNullOrEmpty(thumbnailsPath) && Directory.Exists(thumbnailsPath); - bool tempExists = !string.IsNullOrEmpty(tempPath) && (Directory.Exists(tempPath) || Path.Combine(Path.GetTempPath(), "DazProductInstaller") == tempPath); - bool databaseExists = !string.IsNullOrEmpty(databasePath) && Directory.Exists(databasePath); - bool anyNotEmpty = !string.IsNullOrEmpty(databasePath) || - !string.IsNullOrEmpty(tempPath) || - !string.IsNullOrEmpty(thumbnailsPath) || - !string.IsNullOrEmpty(destinationPath); - invalidSettings = anyNotEmpty && (!destExists || !thumbExists || !tempExists || !databaseExists); - if (!destExists) - { - if (DPRegistry.ContentDirectories.Length == 0) - { - MessageBox.Show("Couldn't find DAZ directories located in registry. On the next prompt, please select where you want your products to be installed to. You can always change this later in the settings.", - "No Daz content directories found in registry", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); - var path = Settings.settingsPage.AskForDirectory(); - while (path == string.Empty) - { - MessageBox.Show("No directory was selected. It is required that you select a directory for products you wish to install. Please select where you want your products to be installed to. You can always change this later in the settings.", "Folder selection required", MessageBoxButtons.OK, MessageBoxIcon.Error); - path = Settings.settingsPage.AskForDirectory(); - } - } - else - { - destinationPath = DPRegistry.ContentDirectories[0]; - } - } - if (!thumbExists) - { - thumbnailsPath = "Thumbnails"; - try - { - Directory.CreateDirectory(thumbnailsPath); - } - catch (Exception ex) - { - DPCommon.WriteToLog($"Failed to create directories for default thumbnail path. REASON: {ex}"); - } - } - if (!tempExists) - { - tempPath = Path.Combine(Path.GetTempPath(), "DazProductInstaller"); - try - { - Directory.CreateDirectory(tempPath); - } - catch (Exception ex) - { - DPCommon.WriteToLog($"Failed to create directories for default temp path. REASON: {ex}"); - } - } - if (!databaseExists) - { - databasePath = "Database"; - try - { - Directory.CreateDirectory(tempPath); - } - catch (Exception ex) - { - DPCommon.WriteToLog($"Failed to create directories for default database path. REASON: {ex}"); - } - } - if (invalidSettings) MessageBox.Show("Some paths are invalid and have been reverted to default.", "Settings defaulted", - MessageBoxButtons.OK, MessageBoxIcon.Information); - } - - public bool SaveSettings() - { - try - { - var s = JsonConvert.SerializeObject(currentSettingsObject, Formatting.Indented); - using var file = File.CreateText(SETTINGS_PATH); - file.Write(s); - return true; - } - catch (Exception ex) - { - DPCommon.WriteToLog($"Failed to parse settings. REASON: {ex}"); - } - return false; - } - - } -} diff --git a/src/DP/DPTaskManager.cs b/src/DP/DPTaskManager.cs deleted file mode 100644 index 8351b38..0000000 --- a/src/DP/DPTaskManager.cs +++ /dev/null @@ -1,276 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace DAZ_Installer.DP -{ - // Notes about Tasks, TaskFactory: - // (1) Token still works even after being disposed. - // (2) Source still semi-works after dispose call. Token is "disposed". - // (3) Tasks will continue to run unless you explicitly use the token and current task scheduler. - internal struct DPTaskManager - { - internal delegate void QueueAction(CancellationToken token); - internal delegate void QueueAction(T arg1, CancellationToken token); - internal delegate void QueueAction(T1 arg1, T2 arg2, CancellationToken token); - internal delegate void QueueAction(T1 arg1, T2 arg2, T3 arg3, CancellationToken token); - internal delegate void QueueAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, CancellationToken token); - internal delegate void QueueAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, CancellationToken token); - - - - private CancellationTokenSource _source; - private TaskFactory _taskFactory; - private CancellationToken _token; - private volatile Task lastTask; - // (3) Tasks will continue with continueWith() chain unless this is passed in. - private const TaskContinuationOptions _continuationOptions = TaskContinuationOptions.NotOnCanceled; - - // According to documentation, Scheduler may (and usually is) null, if null use Current property. - // Check each time by using property instead of raw field. - private TaskScheduler _scheduler { - get => _taskFactory.Scheduler ?? TaskScheduler.Current; - } - - public DPTaskManager() - { - _source = new CancellationTokenSource(); - _token = _source.Token; - _taskFactory = new TaskFactory(_token); - lastTask = null; - } - - private void Reset() - { - _source.Dispose(); - _source = new CancellationTokenSource(); - _token = _source.Token; - _taskFactory = new TaskFactory(_token); - lastTask = null; - } - - public void Stop() - { - _source.Cancel(); - Reset(); - } - - public void StopAndWait() - { - _source.Cancel(); - lastTask?.Wait(); - Reset(); - } - #region Queue methods - - public Task AddToQueue(Action action) - { - CancellationToken t = _token; - Task task; - if (lastTask == null) - { - task = lastTask = Task.Factory.StartNew(action, _token); - } else - { - task = lastTask = lastTask.ContinueWith((_) => action(), t, _continuationOptions, _scheduler); - } - return task; - } - public Task AddToQueue(QueueAction action) - { - CancellationToken t = _token; - Task task; - if (lastTask == null) - { - task = lastTask = Task.Factory.StartNew(() => action(t)); - } else - { - task = lastTask = lastTask.ContinueWith((_) => action(t), t,_continuationOptions, _scheduler); - } - return task; - } - - public Task AddToQueue(QueueAction action, T arg) - { - CancellationToken t = _token; - Task task; - if (lastTask == null) - { - task = lastTask = Task.Factory.StartNew(() => action(arg, t)); - } - else - { - task = lastTask = lastTask.ContinueWith((_) => action(arg, t), t, _continuationOptions, _scheduler); - } - return task; - } - - - public Task AddToQueue(QueueAction action, T1 arg1, T2 arg2) - { - CancellationToken t = _token; - Task task; - if (lastTask == null) - { - task = lastTask = Task.Factory.StartNew(() => action(arg1, arg2, t)); - } - else - { - task = lastTask = lastTask.ContinueWith((_) => action(arg1, arg2, t), t, _continuationOptions, _scheduler); - } - return task; - } - - public Task AddToQueue(QueueAction action, T1 arg1, T2 arg2, T3 arg3) - { - CancellationToken t = _token; - Task task = lastTask; - if (lastTask == null) - { - task = lastTask = Task.Factory.StartNew(() => action(arg1, arg2, arg3, t)); - } - else - { - task = lastTask = lastTask.ContinueWith((_) => action(arg1, arg2, arg3, t), - t, _continuationOptions, _scheduler); - } - return task; - } - - public Task AddToQueue(QueueAction action, T1 arg1, T2 arg2, T3 arg3, T4 arg4) - { - CancellationToken t = _token; - Task task = lastTask; - - if (lastTask == null) - { - task = lastTask = Task.Factory.StartNew(() => action(arg1, arg2, arg3, arg4, t)); - } - else - { - task = lastTask = lastTask.ContinueWith((_) => action(arg1, arg2, arg3, arg4, t), - t, _continuationOptions, _scheduler); - } - return task; - } - - public Task AddToQueue(QueueAction action, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) - { - CancellationToken t = _token; - Task task = lastTask; - - if (lastTask == null) - { - task = lastTask = Task.Factory.StartNew(() => action(arg1, arg2, arg3, arg4, arg5, t)); - } - else - { - task = lastTask = lastTask.ContinueWith((_) => action(arg1, arg2, arg3, arg4, arg5, t), - t, _continuationOptions, _scheduler); - } - return task; - } - - public Task AddToQueue(Func func) - { - CancellationToken t = _token; - Task task = lastTask; - - if (lastTask == null) - { - task = lastTask = Task.Factory.StartNew(() => func(t)); - } - else - { - task = lastTask = lastTask.ContinueWith((_) => func(t), - t, _continuationOptions, _scheduler); - } - return task; - } - public Task AddToQueue(Func func, T1 arg1) - { - CancellationToken t = _token; - Task task = lastTask; - if (lastTask == null) - { - task = lastTask = Task.Factory.StartNew(() => func(arg1, t)); - } - else - { - task = lastTask = lastTask.ContinueWith((_) => func(arg1, t), - t, _continuationOptions, _scheduler); - } - return task; - } - public Task AddToQueue(Func func, T1 arg1, T2 arg2) - { - CancellationToken t = _token; - Task task = lastTask; - if (lastTask == null) - { - task = lastTask = Task.Factory.StartNew(() => func(arg1, arg2, t)); - } - else - { - task = lastTask = lastTask.ContinueWith((_) => func(arg1, arg2, t), - t, _continuationOptions, _scheduler); - } - return task; - } - public Task AddToQueue(Func func, - T1 arg1, T2 arg2, T3 arg3) - { - CancellationToken t = _token; - Task task = lastTask; - if (lastTask == null) - { - task = lastTask = Task.Factory.StartNew(() => func(arg1, arg2, arg3, t)); - } - else - { - task = lastTask = lastTask.ContinueWith((_) => func(arg1, arg2, arg3, t), - t, _continuationOptions, _scheduler); - } - return task; - } - public Task AddToQueue(Func func, - T1 arg1, T2 arg2, T3 arg3, T4 arg4) - { - CancellationToken t = _token; - Task task = lastTask; - if (lastTask == null) - { - task = lastTask = Task.Factory.StartNew(() => func(arg1, arg2, arg3, arg4, t)); - } - else - { - task = lastTask = lastTask.ContinueWith((_) => func(arg1, arg2, arg3, arg4, t), - t, _continuationOptions, _scheduler); - } - return task; - } - public Task AddToQueue(Func func, - T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) - { - CancellationToken t = _token; - Task task = lastTask; - if (lastTask == null) - { - task = lastTask = Task.Factory.StartNew(() => func(arg1, arg2, arg3, arg4, arg5, t)); - } - else - { - task = lastTask = lastTask.ContinueWith((_) => func(arg1, arg2, arg3, arg4, arg5, t), - t, _continuationOptions, _scheduler); - } - return task; - } - #endregion - - - - } -} diff --git a/src/DP/DPZipArchive.cs b/src/DP/DPZipArchive.cs deleted file mode 100644 index 8385e86..0000000 --- a/src/DP/DPZipArchive.cs +++ /dev/null @@ -1,202 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE -using IOPath = System.IO.Path; -using System.IO; -using System.IO.Compression; -using System.Text; -using System; - -namespace DAZ_Installer.DP { - internal class DPZipArchive : DPAbstractArchive - { - internal override bool CanReadWithoutExtracting { get => true; } - private ZipArchive archive; - - internal DPZipArchive(string _path, bool innerArchive = false, string? relativePathBase = null) : base(_path, innerArchive, relativePathBase) { - - } - - ~DPZipArchive() - { - // Release the handle of the zip archive if it isn't null. - archive?.Dispose(); - } - - #region Override Methods - - internal override void Extract() - { - mode = Mode.Extract; - var max = GetExpectedFilesToExtract(); - // Indicates that nothing here should be extracted. - if (max == 0) - { - HandleProgressionZIP(archive, 1, 1); - return; - } - var i = 0; - foreach (var file in archive.Entries) { - try - { - DPFile dpfile = null; - DPAbstractArchive arc = InternalArchives.Find(a => a.Path == file.FullName); - if (DPFile.FindFileInDPFiles(file.FullName, out dpfile)) - { - if (dpfile.WillExtract) - { - ExtractFile(file, dpfile); - HandleProgressionZIP(archive, ++i, max); - } - } - if (arc != null && arc.WillExtract) - ExtractFile(file, arc); - } catch (Exception ex) - { - DPCommon.WriteToLog($"Failed to extract {file.FullName}. REASON: {ex}"); - } - - } - HandleProgressionZIP(archive, max, max); - } - - internal override void Peek() - { - archive = ZipFile.OpenRead(IsInnerArchive ? ExtractedPath : Path); - foreach (var entry in archive.Entries) { - if (string.IsNullOrEmpty(entry.Name)) { - // It is a folder. - if (!FolderExists(entry.FullName)) - { - var folder = new DPFolder(entry.FullName, null); - folder.AssociatedArchive = this; - } - - } - else if (DPFile.ValidImportExtension(GetExtension(entry.Name))) { - var newArchive = CreateNewArchive(entry.FullName, true); - newArchive.ParentArchive = this; - Contents.Add(newArchive); - } else { - var newFile = DPFile.CreateNewFile(entry.FullName, null); - newFile.AssociatedArchive = this; - } - TrueArchiveSize += (ulong) Math.Max((long) 0, entry.Length); - } - } - - internal override void ReadContentFiles() - { - foreach (var file in DazFiles) { - if (!file.WasExtracted) continue; - try { - using (var stream = new FileStream(file.ExtractedPath, FileMode.Open)) { - if (stream.ReadByte() == 0x1F && stream.ReadByte() == 0x8B) { - // It is gzipped compressed. - stream.Seek(0, SeekOrigin.Begin); - using (var gstream = new GZipStream(stream, CompressionMode.Decompress)) { - using (var streamReader = new StreamReader(gstream, Encoding.UTF8, true)) { - file.ReadContents(streamReader); - } - } - } else { - // It is normal text. - stream.Seek(0, SeekOrigin.Begin); - using (var streamReader = new StreamReader(stream, Encoding.UTF8, true)) { - file.ReadContents(streamReader); - } - } - } - - } catch {} - } - } - - internal override void ReadMetaFiles() - { - foreach (var file in DSXFiles) { - try - { - var entry = archive.GetEntry(file.Path); - if (entry == null) continue; - ExtractFile(entry, file); - if (file.WasExtracted) { - file.CheckContents(); - } - } catch (Exception ex) - { - DPCommon.WriteToLog($"Unable to read meta file {file.Path}. REASON: {ex}"); - } - } - } - - internal override void ReleaseArchiveHandles() - { - archive?.Dispose(); - } - - #endregion - - internal int GetExpectedFilesToExtract() { - int count = 0; - foreach (var content in Contents) { - if (content.WillExtract) count++; - } - return count; - } - - private void ExtractFile(ZipArchiveEntry entry, DPAbstractFile file) { - bool fixedAttribute = false; - EXTRACT: - string expectedPath = file.TargetPath ?? IOPath.Combine(DPProcessor.TempLocation, IOPath.GetFileNameWithoutExtension(Path), entry.Name); - try { - try { - Directory.CreateDirectory(IOPath.GetDirectoryName(expectedPath)); - } catch {} - - entry.ExtractToFile(expectedPath, DPProcessor.OverwriteFiles == SettingOptions.Yes); - file.WasExtracted = true; - file.ExtractedPath = expectedPath; - } catch (IOException e) - { - if (e.Message.StartsWith("The file ") && e.Message.EndsWith("already exists")) - { - DPCommon.WriteToLog("The extracted file already existed but user chose not to overwrite files."); - } - // Note: System.UnauthorizedAccessException can occur when zip is attempting to overwrite a hidden and/or read-only file. - } catch (UnauthorizedAccessException) - { - // Try setting the attributes to normal and see what happens. - try - { - var fileInfo = new FileInfo(expectedPath); - if (fileInfo.Exists && !fixedAttribute) - { - fileInfo.Attributes = FileAttributes.Normal; - fixedAttribute = true; - goto EXTRACT; - } - else - DPCommon.WriteToLog($"Failed to extract file even after file attribute change for {entry.FullName}."); - } - catch (Exception ex) - { - DPCommon.WriteToLog($"Unable to extract file and change file attributes for {entry.FullName}. REASON: {ex}"); - } - } - catch (Exception e) { - DPCommon.WriteToLog($"Unable to extract file: {entry.FullName}. Reason: {e}"); - } - } - - public void HandleProgressionZIP(ZipArchive sender, int i, int max) - { - i = Math.Min(i, max); - if (ProgressCombo == null) ProgressCombo = new DPProgressCombo(); - var percentComplete = (float)i / max; - var progress = (int)Math.Floor(percentComplete * 100); - ProgressCombo.UpdateText($"Extracting files...({progress}%)"); - ProgressCombo.ProgressBar.Value = progress; - } - - } -} \ No newline at end of file diff --git a/src/DP/Program.cs b/src/DP/Program.cs deleted file mode 100644 index 55c90e8..0000000 --- a/src/DP/Program.cs +++ /dev/null @@ -1,53 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE - -using System; -using System.Threading; -using System.Windows.Forms; -using System.Diagnostics; - -namespace DAZ_Installer -{ - static class Program - { - public static bool IsRunByIDE => Debugger.IsAttached; - /// - /// The main entry point for the application. - /// - [STAThread] - static void Main() - { - if (CheckInstances()) return; - using var mutex = new Mutex(false, "DAZ_Installer Instance"); - mutex.WaitOne(0); - // Set the main thread ID to this one. - DP.DPGlobal.mainThreadID = Thread.CurrentThread.ManagedThreadId; - Application.SetHighDpiMode(HighDpiMode.SystemAware); - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - Application.Run(new MainForm()); - mutex.ReleaseMutex(); - } - - /// - /// Checks if there is a instance of the application running. - /// - /// True if there the app is already running, otherwise false. - static bool CheckInstances() - { - using (var mutex = new Mutex(false, "DAZ_Installer Instance")) - { - // Code from: https://saebamini.com/Allowing-only-one-instance-of-a-C-app-to-run/ - bool isAnotherInstanceOpen = !mutex.WaitOne(0); - if (isAnotherInstanceOpen) - { - MessageBox.Show(null, "Only one instance of Daz Product Installer is allowed!", "Launch cancelled", MessageBoxButtons.OK, MessageBoxIcon.Error); - return true; - } - - mutex.ReleaseMutex(); - } - return false; - } - } -} diff --git a/src/DP/UsefulFuncs.cs b/src/DP/UsefulFuncs.cs deleted file mode 100644 index fab0f11..0000000 --- a/src/DP/UsefulFuncs.cs +++ /dev/null @@ -1,326 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE - -using System; -using System.IO; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Windows.Forms; -using System.Threading; - -namespace DAZ_Installer.DP -{ - internal struct DPCommon - { - internal static bool IsOnMainThread { get => - DPGlobal.mainThreadID == - Thread.CurrentThread.ManagedThreadId; - } - internal static DragDropEffects dropEffect = DragDropEffects.All; - public static string Up(string str) - { - if (str == string.Empty) - { - return String.Empty; - } - if (Path.HasExtension(str)) - { - var fileName = PathHelper.GetFileName(str); - return str.Remove(str.LastIndexOf(fileName)).TrimEnd(PathHelper.GetSeperator(str)); - } - else - { - var dirName = PathHelper.GetLastDir(str, false); - if (dirName == "" && PathHelper.GetAbsoluteUpPath(str) != dirName) dirName = PathHelper.GetAbsoluteUpPath(str); - var trimmedPath = str.Remove(str.LastIndexOf(dirName)); - return PathHelper.GetAbsoluteUpPath(trimmedPath); - } - - - } - public static string ConvertToUnicode(string defaultString) - { - // Convert string to bytes. - byte[] bytes = new byte[defaultString.Length]; - for (int i = 0; i < defaultString.Length; ++i) - { - bytes[i] = (byte)defaultString[i]; - } - // Convert default encoding to unicode and output it to bytes. - byte[] unicodeBytes = Encoding.Convert(Encoding.Default, Encoding.Unicode, bytes); - - // Convert Unicode byte array to Unicode string. - return Encoding.Unicode.GetString(unicodeBytes); - } - public static Control[] RecursivelyGetControls(Control obj) - { - if (obj.Controls.Count == 0) - { - return null; - } - else - { - var workingArr = new List(obj.Controls.Count); - foreach (Control control in obj.Controls) - { - var result = RecursivelyGetControls(control); - if (result != null) - { - foreach (Control childControl in result) - { - var _index = ArrayHelper.GetNextOpenSlot(workingArr); - if (_index == -1) - { - workingArr.Add(childControl); - } - else - { - workingArr[_index] = childControl; - } - } - } - var index = ArrayHelper.GetNextOpenSlot(workingArr); - if (index == -1) - { - workingArr.Add(control); - } - else - { - workingArr[index] = control; - } - } - var controlArr = workingArr.OfType().ToArray(); - return controlArr; - } - - } - - - public static void WriteToLog(params object[] args) - { -#if DEBUG - System.Diagnostics.Debug.WriteLine(string.Join(' ', args)); - // TO DO: Call log. -#endif - } - } - - internal readonly struct ArrayHelper - { - internal static int GetIndex(object[] array, object obj) - { - for (var i = 0; i < array.Length; i++) - { - if (array[i] == obj) - { - return i; - } - } - return -1; - } - internal static int GetIndex(T[] array, T obj) - { - if (obj == null) return -1; - for (var i = 0; i < array.Length; i++) - { - if (array[i].Equals(obj)) - { - return i; - } - } - return -1; - } - /// - /// Returns the first null available in given array. - /// Currently O(N) lookup. - /// - /// The array to search for an open slot. - /// Returns the index of the next available slot. Returns -1 if no open slot is found. - internal static int GetNextOpenSlot(object[] array) - { - for (var i = 0; i < array.Length; i++) - { - if (array[i] == null) - { - return i; - } - } - return -1; - } - internal static int GetNextOpenSlot(List array) - { - for (var i = 0; i < array.Count; i++) - { - if (array[i] == null) - { - return i; - } - } - return -1; - } - - internal static bool Contains(string[] array, string obj) - { - for (var i = 0; i < array.Length; i++) - { - // Index of is 5000x faster than Contains() - if (obj.IndexOf(array[i]) != -1) return true; - } - return false; - } - - internal static bool Contains(ICollection array, string obj) - { - foreach (var _obj in array) - { - if (obj.IndexOf(_obj) != -1) return true; - } - return false; - } - internal static void ClearArray(object[] array) => Array.Clear(array); - } - - internal readonly struct PathHelper - { - /// - /// Returns the relative path of the given path. - /// - /// - The absolute path (or partial path) to compare. - /// - The absolute path of the path to compare to minus the sublevel.. - /// The relative path of the given path. - internal static string GetRelativePath(ReadOnlySpan path, ReadOnlySpan relativeTo) - { - char rSeperator = GetSeperator(relativeTo); - char pSeperator = GetSeperator(path); - var pNameSections = path.ToString().Split(pSeperator); // i - var rNameSections = relativeTo.ToString().Split(rSeperator); // j - // We want find the last index of rNameSections - - var findIndex = ArrayHelper.GetIndex(pNameSections, rNameSections[rNameSections.Length - 1]); - if (findIndex == -1) return path.ToString(); - StringBuilder pathBuilder = new StringBuilder(path.Length); - for (int i = findIndex; i < pNameSections.Length; i++) - { - pathBuilder.Append(pNameSections[i] + rSeperator); - } - return pathBuilder.Length == 0 ? string.Empty : pathBuilder.ToString().TrimEnd(rSeperator); - } - - internal static char GetSeperator(ReadOnlySpan path) - { - var forwardSlash = path.LastIndexOf('\\') != -1; - var backwardSlash = path.LastIndexOf('/') != -1; - - if (forwardSlash && !backwardSlash) - { - return '\\'; - } - else return '/'; - } - - - internal static string GetLastDir(string path, bool isFilePath) - { - char seperator = GetSeperator(path); - if (!isFilePath) - { - return path.Split(seperator).Last(); - } - else - { - var arr = path.Split(seperator); - if (arr.Length >= 2 && arr[^1].Contains('.')) - { - return arr[^2]; - } - return arr[0]; - } - } - - internal static string GetParent(string path) - { - char seperator = GetSeperator(path); - var lastSeperatorIndex = path.LastIndexOf(seperator); - return lastSeperatorIndex != -1 ? path.Substring(0, lastSeperatorIndex) : path; - } - - internal static string GetFileName(string path) - { - char seperator = GetSeperator(path); - return path.Split(seperator).Last(); - } - - internal static string GetAbsoluteUpPath(string path) - { - var seperator = GetSeperator(path); - var strBuilder = ""; - foreach (var str in path.Split(seperator)) - { - if (str.Trim() == "") continue; - strBuilder += str + seperator; - } - return strBuilder.TrimEnd(seperator); - } - - internal static byte GetNumOfLevelsAbove(string path, string relativeTo) - { - var relPath = GetRelativePath(path, relativeTo); - var seperator = GetSeperator(relPath); - return (byte)relPath.Count((c) => c == seperator); - } - - internal static byte GetNumOfLevels(string path) - { - var seperator = GetSeperator(path); - return (byte)path.Count((c) => c == seperator); - } - - internal static string SwitchSeperators(string path) - { - try - { - var chars = path.ToCharArray(); - var seperator = GetSeperator(path); - char oppositeSeparator; - - if (seperator == '\\') oppositeSeparator = '/'; - else oppositeSeparator = '\\'; - - for (var i = 0; i < chars.Length; i++) - { - if (chars[i] == seperator) - { - chars[i] = oppositeSeparator; - } - } - return new string(chars); - } - catch { } - return path; - } - - internal static string GetDirectoryPath(string path) - { - var seperator = GetSeperator(path); - var strBuilder = ""; - foreach (var str in path.Split(seperator)) - { - if (str.Trim() == "") continue; - strBuilder += str + seperator; - } - strBuilder = strBuilder.TrimEnd(seperator); - - if (seperator == '/') return SwitchSeperators(strBuilder); - else return strBuilder; - } - - /// - /// Replaces all forward slashes (/) with back slashes (\). - /// - /// The path to cleanize. Cannot be null. - /// - internal static string NormalizePath(string path) => path.Replace('/', '\\'); - } - -} diff --git a/src/Forms/AboutForm.Designer.cs b/src/Forms/AboutForm.Designer.cs deleted file mode 100644 index fb05889..0000000 --- a/src/Forms/AboutForm.Designer.cs +++ /dev/null @@ -1,119 +0,0 @@ -namespace DAZ_Installer.Forms -{ - partial class AboutForm - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(AboutForm)); - this.titleLbl = new System.Windows.Forms.Label(); - this.pictureBox1 = new System.Windows.Forms.PictureBox(); - this.mainInfoLbl = new System.Windows.Forms.Label(); - this.licensesRichTxtBox = new System.Windows.Forms.RichTextBox(); - this.licensesLbl = new System.Windows.Forms.Label(); - ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit(); - this.SuspendLayout(); - // - // titleLbl - // - this.titleLbl.AutoSize = true; - this.titleLbl.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point); - this.titleLbl.Location = new System.Drawing.Point(84, 117); - this.titleLbl.Name = "titleLbl"; - this.titleLbl.Size = new System.Drawing.Size(169, 21); - this.titleLbl.TabIndex = 0; - this.titleLbl.Text = "Daz Product Installer"; - // - // pictureBox1 - // - this.pictureBox1.Image = global::DAZ_Installer.Properties.Resources.Logo2_256x; - this.pictureBox1.Location = new System.Drawing.Point(84, 12); - this.pictureBox1.Name = "pictureBox1"; - this.pictureBox1.Size = new System.Drawing.Size(164, 102); - this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; - this.pictureBox1.TabIndex = 1; - this.pictureBox1.TabStop = false; - // - // mainInfoLbl - // - this.mainInfoLbl.Location = new System.Drawing.Point(12, 138); - this.mainInfoLbl.Name = "mainInfoLbl"; - this.mainInfoLbl.Size = new System.Drawing.Size(313, 98); - this.mainInfoLbl.TabIndex = 2; - this.mainInfoLbl.Text = resources.GetString("mainInfoLbl.Text"); - this.mainInfoLbl.TextAlign = System.Drawing.ContentAlignment.TopCenter; - // - // licensesRichTxtBox - // - this.licensesRichTxtBox.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.licensesRichTxtBox.Location = new System.Drawing.Point(12, 262); - this.licensesRichTxtBox.Name = "licensesRichTxtBox"; - this.licensesRichTxtBox.Size = new System.Drawing.Size(313, 108); - this.licensesRichTxtBox.TabIndex = 3; - this.licensesRichTxtBox.Text = "no u"; - // - // licensesLbl - // - this.licensesLbl.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point); - this.licensesLbl.Location = new System.Drawing.Point(12, 236); - this.licensesLbl.Name = "licensesLbl"; - this.licensesLbl.Size = new System.Drawing.Size(313, 23); - this.licensesLbl.TabIndex = 4; - this.licensesLbl.Text = "Licenses"; - this.licensesLbl.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; - // - // AboutForm - // - this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; - this.ClientSize = new System.Drawing.Size(337, 382); - this.Controls.Add(this.licensesLbl); - this.Controls.Add(this.licensesRichTxtBox); - this.Controls.Add(this.mainInfoLbl); - this.Controls.Add(this.pictureBox1); - this.Controls.Add(this.titleLbl); - this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); - this.MaximumSize = new System.Drawing.Size(353, 9999); - this.MinimumSize = new System.Drawing.Size(353, 421); - this.Name = "AboutForm"; - this.Text = "About Form"; - ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit(); - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - - private System.Windows.Forms.Label titleLbl; - private System.Windows.Forms.PictureBox pictureBox1; - private System.Windows.Forms.Label mainInfoLbl; - private System.Windows.Forms.RichTextBox licensesRichTxtBox; - private System.Windows.Forms.Label licensesLbl; - } -} \ No newline at end of file diff --git a/src/Forms/AboutForm.cs b/src/Forms/AboutForm.cs deleted file mode 100644 index fd51e92..0000000 --- a/src/Forms/AboutForm.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Forms; - -namespace DAZ_Installer.Forms -{ - public partial class AboutForm : Form - { - public AboutForm() - { - InitializeComponent(); - } - } -} diff --git a/src/Forms/MainForm.Designer.cs b/src/Forms/MainForm.Designer.cs deleted file mode 100644 index fb9a5bc..0000000 --- a/src/Forms/MainForm.Designer.cs +++ /dev/null @@ -1,251 +0,0 @@ - -namespace DAZ_Installer -{ - partial class MainForm - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); - this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); - this.pictureBox1 = new System.Windows.Forms.PictureBox(); - this.homeLabel = new System.Windows.Forms.Label(); - this.extractLbl = new System.Windows.Forms.Label(); - this.libraryLbl = new System.Windows.Forms.Label(); - this.settingsLbl = new System.Windows.Forms.Label(); - this.mainPanel = new System.Windows.Forms.Panel(); - this.homePage1 = new DAZ_Installer.Home(); - this.extractControl1 = new DAZ_Installer.Extract(); - this.library1 = new DAZ_Installer.Library(); - this.settings1 = new DAZ_Installer.Settings(); - this.openFileDialog = new System.Windows.Forms.OpenFileDialog(); - this.tableLayoutPanel1.SuspendLayout(); - ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit(); - this.mainPanel.SuspendLayout(); - this.SuspendLayout(); - // - // tableLayoutPanel1 - // - this.tableLayoutPanel1.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(53)))), ((int)(((byte)(50)))), ((int)(((byte)(56))))); - this.tableLayoutPanel1.ColumnCount = 1; - this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.tableLayoutPanel1.Controls.Add(this.pictureBox1, 0, 0); - this.tableLayoutPanel1.Controls.Add(this.homeLabel, 0, 1); - this.tableLayoutPanel1.Controls.Add(this.extractLbl, 0, 2); - this.tableLayoutPanel1.Controls.Add(this.libraryLbl, 0, 3); - this.tableLayoutPanel1.Controls.Add(this.settingsLbl, 0, 4); - this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Left; - this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 0); - this.tableLayoutPanel1.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); - this.tableLayoutPanel1.Name = "tableLayoutPanel1"; - this.tableLayoutPanel1.RowCount = 5; - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 20F)); - this.tableLayoutPanel1.Size = new System.Drawing.Size(117, 344); - this.tableLayoutPanel1.TabIndex = 0; - // - // pictureBox1 - // - this.pictureBox1.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(53)))), ((int)(((byte)(50)))), ((int)(((byte)(56))))); - this.pictureBox1.Dock = System.Windows.Forms.DockStyle.Fill; - this.pictureBox1.Image = global::DAZ_Installer.Properties.Resources.Logo2_256x; - this.pictureBox1.Location = new System.Drawing.Point(3, 2); - this.pictureBox1.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); - this.pictureBox1.Name = "pictureBox1"; - this.pictureBox1.Size = new System.Drawing.Size(111, 64); - this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; - this.pictureBox1.TabIndex = 0; - this.pictureBox1.TabStop = false; - this.pictureBox1.Click += new System.EventHandler(this.pictureBox1_Click); - // - // homeLabel - // - this.homeLabel.AutoSize = true; - this.homeLabel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(53)))), ((int)(((byte)(50)))), ((int)(((byte)(56))))); - this.homeLabel.Dock = System.Windows.Forms.DockStyle.Fill; - this.homeLabel.Font = new System.Drawing.Font("Segoe UI Variable Text Light", 18F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.homeLabel.ForeColor = System.Drawing.Color.White; - this.homeLabel.Location = new System.Drawing.Point(3, 68); - this.homeLabel.Name = "homeLabel"; - this.homeLabel.Size = new System.Drawing.Size(111, 68); - this.homeLabel.TabIndex = 1; - this.homeLabel.Text = "Home"; - this.homeLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; - this.homeLabel.Click += new System.EventHandler(this.homeLabel_Click); - this.homeLabel.MouseEnter += new System.EventHandler(this.sidePanelButtonMouseEnter); - this.homeLabel.MouseLeave += new System.EventHandler(this.sidePanelButtonMouseExit); - // - // extractLbl - // - this.extractLbl.AutoSize = true; - this.extractLbl.Dock = System.Windows.Forms.DockStyle.Fill; - this.extractLbl.Font = new System.Drawing.Font("Segoe UI Variable Text Light", 18F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.extractLbl.ForeColor = System.Drawing.Color.White; - this.extractLbl.Location = new System.Drawing.Point(3, 136); - this.extractLbl.Name = "extractLbl"; - this.extractLbl.Size = new System.Drawing.Size(111, 68); - this.extractLbl.TabIndex = 2; - this.extractLbl.Text = "Extract"; - this.extractLbl.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; - this.extractLbl.Click += new System.EventHandler(this.extractLbl_Click); - this.extractLbl.MouseEnter += new System.EventHandler(this.sidePanelButtonMouseEnter); - this.extractLbl.MouseLeave += new System.EventHandler(this.sidePanelButtonMouseExit); - // - // libraryLbl - // - this.libraryLbl.AutoSize = true; - this.libraryLbl.Dock = System.Windows.Forms.DockStyle.Fill; - this.libraryLbl.Font = new System.Drawing.Font("Segoe UI Variable Text Light", 18F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.libraryLbl.ForeColor = System.Drawing.Color.White; - this.libraryLbl.Location = new System.Drawing.Point(3, 204); - this.libraryLbl.Name = "libraryLbl"; - this.libraryLbl.Size = new System.Drawing.Size(111, 68); - this.libraryLbl.TabIndex = 3; - this.libraryLbl.Text = "Library"; - this.libraryLbl.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; - this.libraryLbl.Click += new System.EventHandler(this.libraryLbl_Click); - this.libraryLbl.MouseEnter += new System.EventHandler(this.sidePanelButtonMouseEnter); - this.libraryLbl.MouseLeave += new System.EventHandler(this.sidePanelButtonMouseExit); - // - // settingsLbl - // - this.settingsLbl.AutoSize = true; - this.settingsLbl.Dock = System.Windows.Forms.DockStyle.Fill; - this.settingsLbl.Font = new System.Drawing.Font("Segoe UI Variable Text Light", 18F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.settingsLbl.ForeColor = System.Drawing.Color.White; - this.settingsLbl.Location = new System.Drawing.Point(3, 272); - this.settingsLbl.Name = "settingsLbl"; - this.settingsLbl.Size = new System.Drawing.Size(111, 72); - this.settingsLbl.TabIndex = 4; - this.settingsLbl.Text = "Settings"; - this.settingsLbl.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; - this.settingsLbl.Click += new System.EventHandler(this.settingsLbl_Click); - this.settingsLbl.MouseEnter += new System.EventHandler(this.sidePanelButtonMouseEnter); - this.settingsLbl.MouseLeave += new System.EventHandler(this.sidePanelButtonMouseExit); - // - // mainPanel - // - this.mainPanel.Controls.Add(this.homePage1); - this.mainPanel.Controls.Add(this.extractControl1); - this.mainPanel.Controls.Add(this.library1); - this.mainPanel.Controls.Add(this.settings1); - this.mainPanel.Dock = System.Windows.Forms.DockStyle.Fill; - this.mainPanel.Location = new System.Drawing.Point(117, 0); - this.mainPanel.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); - this.mainPanel.Name = "mainPanel"; - this.mainPanel.Size = new System.Drawing.Size(542, 344); - this.mainPanel.TabIndex = 1; - // - // homePage1 - // - this.homePage1.AllowDrop = true; - this.homePage1.AutoSize = true; - this.homePage1.BackColor = System.Drawing.Color.White; - this.homePage1.Dock = System.Windows.Forms.DockStyle.Fill; - this.homePage1.Location = new System.Drawing.Point(0, 0); - this.homePage1.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); - this.homePage1.MinimumSize = new System.Drawing.Size(494, 294); - this.homePage1.Name = "homePage1"; - this.homePage1.Size = new System.Drawing.Size(542, 344); - this.homePage1.TabIndex = 0; - // - // extractControl1 - // - this.extractControl1.BackColor = System.Drawing.Color.White; - this.extractControl1.Dock = System.Windows.Forms.DockStyle.Fill; - this.extractControl1.Location = new System.Drawing.Point(0, 0); - this.extractControl1.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); - this.extractControl1.Name = "extractControl1"; - this.extractControl1.Size = new System.Drawing.Size(542, 344); - this.extractControl1.TabIndex = 1; - // - // library1 - // - this.library1.BackColor = System.Drawing.Color.White; - this.library1.Dock = System.Windows.Forms.DockStyle.Fill; - this.library1.Location = new System.Drawing.Point(0, 0); - this.library1.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); - this.library1.Name = "library1"; - this.library1.Size = new System.Drawing.Size(542, 344); - this.library1.TabIndex = 2; - // - // settings1 - // - this.settings1.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(192)))), ((int)(((byte)(255)))), ((int)(((byte)(192))))); - this.settings1.Dock = System.Windows.Forms.DockStyle.Fill; - this.settings1.Location = new System.Drawing.Point(0, 0); - this.settings1.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); - this.settings1.Name = "settings1"; - this.settings1.Size = new System.Drawing.Size(542, 344); - this.settings1.TabIndex = 2; - // - // openFileDialog - // - this.openFileDialog.FileName = "openFileDialog1"; - this.openFileDialog.SupportMultiDottedExtensions = true; - // - // MainForm - // - this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; - this.ClientSize = new System.Drawing.Size(659, 344); - this.Controls.Add(this.mainPanel); - this.Controls.Add(this.tableLayoutPanel1); - this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); - this.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); - this.MinimumSize = new System.Drawing.Size(675, 383); - this.Name = "MainForm"; - this.Text = "Daz Product Installer"; - this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing); - this.Load += new System.EventHandler(this.Form1_Load); - this.tableLayoutPanel1.ResumeLayout(false); - this.tableLayoutPanel1.PerformLayout(); - ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit(); - this.mainPanel.ResumeLayout(false); - this.mainPanel.PerformLayout(); - this.ResumeLayout(false); - - } - - #endregion - private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; - private System.Windows.Forms.PictureBox pictureBox1; - private System.Windows.Forms.Label homeLabel; - private System.Windows.Forms.Panel mainPanel; - private Home homePage1; - private System.Windows.Forms.Label extractLbl; - private System.Windows.Forms.Label libraryLbl; - private System.Windows.Forms.Label settingsLbl; - public Extract extractControl1; - private Library library1; - private Settings settings1; - internal System.Windows.Forms.OpenFileDialog openFileDialog; - } -} - diff --git a/src/Utilities/ListExtensions.cs b/src/Utilities/ListExtensions.cs deleted file mode 100644 index 1fdd7b7..0000000 --- a/src/Utilities/ListExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Reflection; -using System.Collections.Generic; - -namespace DAZ_Installer.Utilities -{ - internal static class ListExtensions - { - public static T[]? GetInnerArray(this List list) - { - return list.GetType() - .GetField("_items", BindingFlags.Instance | BindingFlags.NonPublic) - .GetValue(list) as T[]; - } - } -}