Compare commits

..

268 Commits
v0.0.9 ... main

Author SHA1 Message Date
Pat Hartl 5ef16fc4cc Add missing collection model 2023-12-04 01:42:02 -06:00
Pat Hartl db8d3e4bf6 Add role pane to settings with ability to add/delete roles 2023-12-04 01:41:49 -06:00
Pat Hartl f2462c0d20 Switch change password icon to lock 2023-12-04 01:41:02 -06:00
Pat Hartl b87fe92c63 Order tag inputs by the label 2023-12-04 01:40:43 -06:00
Pat Hartl c735556281 Add collections to navigation 2023-12-04 01:40:28 -06:00
Pat Hartl d4bcde9d28 Merge branch 'main' of https://github.com/pathartl/LANCommander 2023-12-04 01:40:13 -06:00
Pat Hartl 920b6b26f7 Added collections with the ability to add games to a collection 2023-12-04 01:39:59 -06:00
Pat Hartl d31fccc9f3
Update installermanifest.yaml 2023-12-03 21:12:35 -06:00
Pat Hartl c48d5b5d59 Add migration to fix PowerShell scripts that were updated to use Get-PrimaryDisplay 2023-12-03 14:29:51 -06:00
Pat Hartl dfb9f51acd Remove unused dialog code. Prefix action file picker with {InstallDir} 2023-12-03 14:16:56 -06:00
Pat Hartl 03b0d9da93 Remove Windows-only dashboard charts. Add overall playtime chart. 2023-12-03 14:09:48 -06:00
Pat Hartl e2883322e6 Display total number of games as subtitle 2023-12-03 02:35:41 -06:00
Pat Hartl 50eff6784f Load data a little later on game edit screen. Fix TagsInput sometimes losing data. 2023-12-02 23:48:36 -06:00
Pat Hartl 171f82df3e Fix package references 2023-12-02 23:08:32 -06:00
Pat Hartl 8b7be9157e Import YamlDorNet from Playnite 2023-12-02 18:40:43 -06:00
Pat Hartl 96c6d58486 Revert "Update LANCommander.Release.yml"
This reverts commit 6fb77adb63.
2023-12-02 18:38:09 -06:00
Pat Hartl fd6e3e56a4 Revert "Update LANCommander.Release.yml"
This reverts commit ba46970406.
2023-12-02 18:38:05 -06:00
Pat Hartl d4fe8db48d Add regex flag to save paths 2023-12-02 17:53:54 -06:00
Pat Hartl 281353b86c Remove regex save path database migration 2023-12-02 17:52:08 -06:00
Pat Hartl ba46970406
Update LANCommander.Release.yml 2023-12-02 14:32:06 -06:00
Pat Hartl 6fb77adb63
Update LANCommander.Release.yml 2023-12-02 14:23:17 -06:00
Pat Hartl b38f2b6cef Allow padding of strings by setting a MinLength when converting to bytes 2023-12-01 18:32:43 -06:00
Pat Hartl 7568688c97 Add search, page size, and page index to router 2023-12-01 18:21:10 -06:00
Pat Hartl a8c62151f3 Fix snippets 2023-11-30 19:46:04 -06:00
Pat Hartl 32e135de7b Merge branch 'net8.0' 2023-11-30 19:14:48 -06:00
Pat Hartl c37095d4c4 Merge branch 'main' into net8.0 2023-11-30 19:12:56 -06:00
Pat Hartl a716bafc4d Merge branch 'save-path-regex' 2023-11-30 19:12:47 -06:00
Pat Hartl e4531321b1 Display error message if script couldn't save. Stop success from blocking dialog close. 2023-11-30 19:12:25 -06:00
Pat Hartl 78168e94a5 Fix new scripts not being created 2023-11-30 19:08:20 -06:00
Pat Hartl 82779bcc72 Restore files from save archive based on entries in manifest 2023-11-30 18:37:23 -06:00
Pat Hartl 7b625b2f60 Use Path.DirectorySeparatorChar 2023-11-30 18:31:05 -06:00
Pat Hartl 74d8790ee2 Fix files skipped over if not in install directory 2023-11-30 18:30:15 -06:00
Pat Hartl 745cf0cce6 Prefer Path.DirectorySeparatorChar. Remove unused deserializer 2023-11-30 18:29:11 -06:00
Pat Hartl 7c22aaa139 Record save file paths in manifest. Support regex pathing in upload 2023-11-30 18:25:37 -06:00
Pat Hartl eb73885991 Add unknown to save file download if game doesn't exist. Add .zip file extension 2023-11-30 17:31:40 -06:00
Pat Hartl ce80dfa51f Fix pathing for save in download 2023-11-30 17:31:11 -06:00
Pat Hartl 349001d8f6 Merge branch 'main' into save-path-regex 2023-11-30 17:23:40 -06:00
Pat Hartl d705b34f84 Updated test packages 2023-11-30 17:22:54 -06:00
Pat Hartl 0246ee017c Update ZstdSharp 2023-11-30 00:16:25 -06:00
Pat Hartl 818160d658 Update to .NET 8.0 2023-11-30 00:16:07 -06:00
Pat Hartl 035c98cd18 Initialize save controller 2023-11-30 00:10:54 -06:00
Pat Hartl d3e13aee9e Fix variable snippets 2023-11-29 18:19:28 -06:00
Pat Hartl ce402cf5c1 Refactored archive editor to not bind archives and handle service calls through uploader.
Fixes #40
2023-11-29 18:07:10 -06:00
Pat Hartl 3dbee36886 Remove binding for scripts on script editor for redists 2023-11-29 17:10:48 -06:00
Pat Hartl 7793c9a1e8 Fix button spacing on script editor table 2023-11-29 17:10:25 -06:00
Pat Hartl c4c25ad85b Allow redist detection scripts to run as admin 2023-11-29 17:09:52 -06:00
Pat Hartl 1d2f82fdef Switch redistibutable edit button to link 2023-11-29 17:09:04 -06:00
Pat Hartl a533e9ad8c Fix redistributable downloading 2023-11-29 17:08:33 -06:00
Pat Hartl c3a5edbe46 Update SharpCompress to latest version and rework cancellation 2023-11-28 21:20:07 -06:00
Pat Hartl 8cc97f9bdb Spliit the script editor into its own dialog separate from the table. Only pass IDs between game, editor, and dialog for better performance. 2023-11-28 20:48:03 -06:00
Pat Hartl 875b7b7caa Throw error messages when exception occurs while browsing an archive 2023-11-28 20:46:14 -06:00
Pat Hartl 6cc947b47e Fix adding new archive for redist not showing upon completion
Fixes #42
2023-11-28 19:39:41 -06:00
Pat Hartl a450ac4a18 Fix included packages in Playnite release builds 2023-11-28 18:56:09 -06:00
Pat Hartl ffa24dbecc Fix game deletions not working due to relationship with game saves, save paths, and play sessions 2023-11-28 17:56:16 -06:00
Pat Hartl d0d6701380 Push alert when there is an error deleting a game 2023-11-27 20:28:51 -06:00
Pat Hartl ab67092c2f Fix metadata lookup modal styling 2023-11-22 17:23:21 -06:00
Pat Hartl d97e1f48b3 Fix IGDB check for single player mode 2023-11-22 17:19:15 -06:00
Pat Hartl 50badc981b Update Playntie installer manifest 2023-11-20 19:40:40 -06:00
Pat Hartl b863080842 Fix save errors blocking play session recording. Fix route that SDK client hits for play sessions 2023-11-20 19:33:48 -06:00
Pat Hartl 8b3f2c6cde Force admin authorization for server index and edit pages
Fixes #39
2023-11-20 18:23:37 -06:00
Pat Hartl 70674f900e Fix invalid manifests on disk throwing parsing errors when installing game
Fixes #38
2023-11-20 18:20:34 -06:00
Pat Hartl f21bf4801e Fix invalid SQL in migration for scripts 2023-11-20 18:19:45 -06:00
Pat Hartl ead2c9c3f1 Implement ILogger abstraction for Playnite 2023-11-20 18:19:31 -06:00
Pat Hartl fe0bdf31f6 Add additional script migrations 2023-11-20 17:35:49 -06:00
Pat Hartl 282a1f7c36 Fix authentication status label not updating after authentication 2023-11-20 17:35:11 -06:00
Pat Hartl 5e3384b4fd Avoid exceptions in Write-ReplaceContentInFile 2023-11-20 17:34:33 -06:00
Pat Hartl bc30cc911a Find LANCommander PowerShell module manifest using search
Probably pretty hacky. Environment.CurrentDirectory doesn't seem to be reliable in dev vs prod. In dev it correctly matches the LANCommander.PlaynitePlugin.dll's location. In prod it is the main directory of Playnite.
2023-11-17 22:35:17 -06:00
Pat Hartl 737f2bec84 Fix how arguments are passed to scripts 2023-11-17 21:39:28 -06:00
Pat Hartl a47b77cc5c Run scripts as admin if directed via script contents 2023-11-17 21:39:01 -06:00
Pat Hartl f19ef09ff8 Force IgnoreWow64Redirection in scripts 2023-11-17 21:38:37 -06:00
Pat Hartl 8a408f3a18 Actually fix manifest to include LANCommander.SDK.dll 2023-11-17 21:38:11 -06:00
Pat Hartl 96fd12a72f Include RestSharp and YamlDotNet in extension build 2023-11-17 21:37:55 -06:00
Pat Hartl 1f870366a4 Fix required assembly in PowerShell manifest 2023-11-17 21:34:26 -06:00
Pat Hartl 2a80964fe1 Allow updating of SDK client's server address. Fix Playnite plugin logins when no extension data exists on first start up. 2023-11-17 20:47:11 -06:00
Pat Hartl 6a3c55f669 Fix pathing 2023-11-17 18:05:33 -06:00
Pat Hartl 1f03860360 Include snippets in build 2023-11-17 18:05:26 -06:00
Pat Hartl 314a785fee Rename variable to Display to match previous documentation 2023-11-17 18:04:48 -06:00
Pat Hartl 09d23bcb78 Don't dispose database context for server processes 2023-11-17 12:57:23 -06:00
Pat Hartl 6b005eb384 Backup database before migration 2023-11-17 12:32:09 -06:00
Pat Hartl 39dd24e0ba Don't run through download if game already exists on disk 2023-11-17 11:48:45 -06:00
Pat Hartl 5324723cee Start tracking play sessions 2023-11-17 02:28:46 -06:00
Pat Hartl aff2e991ed
Merge pull request #33 from LANCommander/powershell-enhancements
PowerShell Enhancements
2023-11-17 01:18:41 -06:00
Pat Hartl 14600f5d70 Alter migration to try to automatically convert scripts to new variables where arguments may be missing 2023-11-17 01:16:45 -06:00
Pat Hartl 35fbdd008a Add migration to delete old snippets 2023-11-17 00:54:38 -06:00
Pat Hartl aed9935b16 Updated default snippets 2023-11-17 00:46:05 -06:00
Pat Hartl 2cb7013120 Write final install directory as output for Install-Game. Only process scripts that exist 2023-11-16 15:42:34 -06:00
Pat Hartl 7649e63195 Add test for Convert-AspectRatio 2023-11-16 15:36:45 -06:00
Pat Hartl e723a5345b Scaffold tests for cmdlets 2023-11-16 15:22:20 -06:00
Pat Hartl 982227cf1f Change to inherit from Cmdlet 2023-11-16 15:21:50 -06:00
Pat Hartl 8abc2fa15e Add cmdlet for encoding serialized objects for PS 2023-11-16 15:20:50 -06:00
Pat Hartl ed1b4973d3 Fix cmdlet export specified by module manifest 2023-11-16 14:43:11 -06:00
Pat Hartl 196feedded Load PowerShell module on script execution. Add cmdlet to deserialize/decode variables. 2023-11-16 14:34:00 -06:00
Pat Hartl 2282d9b013 Include PowerShell project in Playnite extension 2023-11-16 14:08:35 -06:00
Pat Hartl 1b72d9002a Switch PowerShell library to 4.6.2. Fix missing includes in project. 2023-11-16 14:07:38 -06:00
Pat Hartl a9f3b7a39d Only execute scripts if they exists on the disk 2023-11-16 14:06:40 -06:00
Pat Hartl 986fb87db1 Make install directory mandatory when uninstalling 2023-11-16 12:06:18 -06:00
Pat Hartl f7fa7aa9f3 Add cmdlet for uninstalling a game 2023-11-16 12:04:55 -06:00
Pat Hartl 49c4b10cf9 Don't get key when uninstalling 2023-11-16 12:04:41 -06:00
Pat Hartl 8bd422249e Fix Install-Game cmdlet name 2023-11-16 12:04:26 -06:00
Pat Hartl 26f03f61fc Add public base URL to LC client 2023-11-16 01:44:35 -06:00
Pat Hartl 839e9b4935 Run scripts after cmdlet install 2023-11-16 01:44:21 -06:00
Pat Hartl c71cf9fedd Cmdlet for installing game 2023-11-16 01:01:17 -06:00
Pat Hartl 86211a7500 Remove unused PowerShell code 2023-11-16 00:47:13 -06:00
Pat Hartl 35f6dadf9c Convert game save registry export commands to inline scripts 2023-11-16 00:47:01 -06:00
Pat Hartl 03828bea60 Remove unused code from uninstall 2023-11-16 00:46:42 -06:00
Pat Hartl 8d85aca0a7 Fix plugin menu items to use new script execution 2023-11-16 00:46:20 -06:00
Pat Hartl 16dc60b90a Run key change script in InstallController 2023-11-16 00:40:27 -06:00
Pat Hartl eb05364542 Fix reference to static method in ScriptHelper 2023-11-15 23:59:12 -06:00
Pat Hartl 97f459eaff Move uninstall script execution to Playnite addon 2023-11-15 23:58:55 -06:00
Pat Hartl cb9f31a00a Use new PowerShell in redistributable installs 2023-11-15 23:53:26 -06:00
Pat Hartl bf2c9ea45a Move script path helpers to ScriptHelper. Execute post-install scripts in Playnite extension. 2023-11-15 23:42:54 -06:00
Pat Hartl 29dcebb70f New PowerShell runtime with the ability to use variables 2023-11-15 22:38:20 -06:00
Pat Hartl baa2b9b206 Started adding PowerShell cmdlets useful for LANCommander scripting 2023-11-13 23:09:06 -06:00
Pat Hartl dc2eff4972 Have manifest writer return the file path 2023-11-13 23:07:50 -06:00
Pat Hartl ae23f621c2 Fix link 2023-11-12 02:20:47 -06:00
Pat Hartl ee62bdf2a1 Updated README 2023-11-12 02:20:24 -06:00
Pat Hartl 5fb4fadfb4 Fix default install directory when logger is provided 2023-11-12 02:10:04 -06:00
Pat Hartl 1a0cff3914
Merge pull request #32 from LANCommander/migrate-installation-to-sdk
Migrate LANCommander Client Code to SDK
2023-11-12 01:53:53 -06:00
Pat Hartl 227411a558 Include ByteSize 2023-11-12 01:52:55 -06:00
Pat Hartl 7c97a3db57 Include download speed in progress dialog 2023-11-12 01:50:34 -06:00
Pat Hartl 6f7c17493c Install redistributables after game is installed 2023-11-12 01:27:15 -06:00
Pat Hartl 81e4848407 Include download percentage in dialog 2023-11-12 01:26:51 -06:00
Pat Hartl 1ede37c031 Simplify game install cancellation. Cancels now happen silently and don't generate a dialog. 2023-11-12 01:04:05 -06:00
Pat Hartl bb980cc063 Avoid exception if manifest is malformed 2023-11-10 21:45:09 -06:00
Pat Hartl 47bb054fd1 Restore progress bar 2023-11-10 21:44:48 -06:00
Pat Hartl ea337dfea1 Feed actual YAML contents to deserializer 2023-11-10 21:37:27 -06:00
Pat Hartl 52a5f5866f Downgrade YamlDotNet 2023-11-10 21:37:02 -06:00
Pat Hartl b77e7f6e53 Pass in default install directory to managers 2023-11-10 21:36:35 -06:00
Pat Hartl 20de9d6cae Allow injection of loggers 2023-11-10 20:53:48 -06:00
Pat Hartl 5237e88612 Null handling for logger 2023-11-10 20:53:28 -06:00
Pat Hartl 73b542856a Refactor GameSaveService into GameSaveManager and SaveController. Update Playnite addon authentication dialogs to use new client. 2023-11-10 01:32:30 -06:00
Pat Hartl 39f2d4b212 Move methods that should be static to ManifestHelper and ScriptHelper. Move install logic to GameManager and RedistributableManager. Update InstallController and UninstallController 2023-11-10 00:29:16 -06:00
Pat Hartl e53709334c Make PowerShellRuntime static 2023-11-09 23:45:37 -06:00
Pat Hartl a679fae0cb Relocate crucial installation logic to SDK 2023-11-09 19:40:38 -06:00
Pat Hartl ff6f9997f5 Added game ID to manifest 2023-11-09 19:38:32 -06:00
Pat Hartl 5d5e137e18 Add flag to mark save path as regex 2023-11-08 20:43:18 -06:00
Pat Hartl 81f8d55694 Fix archive default sort order 2023-11-08 20:25:41 -06:00
Pat Hartl c4793daf07 Fix exception in transfer input if no values are supplied 2023-11-08 19:10:31 -06:00
Pat Hartl 5c4f81cf80
Update installermanifest.yaml 2023-11-06 02:17:41 -06:00
Pat Hartl d111be9828 Remove deprecated server HTTP options 2023-11-05 15:08:09 -06:00
Pat Hartl 63ae3a4f2a Filter out consoles from monitor menu if they haven't been created 2023-11-05 15:06:08 -06:00
Pat Hartl 4b40445bac Fix server consoles tab 2023-11-05 14:45:02 -06:00
Pat Hartl 25de79eeed Handle margin for icons in list by CSS 2023-11-05 12:59:44 -06:00
Pat Hartl 16ba48ed6c Add a bit of margin to icon in server list 2023-11-05 12:56:53 -06:00
Pat Hartl 508f5c18fb Add a bit of margin to icon in server game select 2023-11-05 12:56:05 -06:00
Pat Hartl 53276542e8 Fix icons in server game select 2023-11-05 12:55:09 -06:00
Pat Hartl aec4342188 Update packages 2023-11-05 01:21:43 -06:00
Pat Hartl 2d86ff2518 Restructure redistributable panels to fix initially-empty archive table 2023-11-05 01:00:13 -06:00
Pat Hartl 4393c8fdff Remove empty 2023-11-05 01:31:37 -05:00
Pat Hartl bd15ceaa5b Avoid exception being thrown when server status updates from service 2023-11-04 22:22:37 -05:00
Pat Hartl 03626a75d0 Remove foreaching table 2023-11-04 20:12:11 -05:00
Pat Hartl 78fb812a74 Add descriptiive text for HTTP paths and add a button to get the URL for a path 2023-11-04 20:02:09 -05:00
Pat Hartl 4fb11c1dd7 Change server HTTP options to allow multiple specified paths.
This is useful if you only want to share specific paths via HTTP, or have the paths stored in multiple places and want to unite them under one URL structure.
2023-11-04 19:43:38 -05:00
Pat Hartl d6eff92835 Add submenus for settings and profile. Add logout button. 2023-11-03 23:33:27 -05:00
Pat Hartl 3f3d5b718b Add settings page for media 2023-11-03 01:49:43 -05:00
Pat Hartl 1689cab3b3 Update installer manifest 2023-11-03 01:29:04 -05:00
Pat Hartl f275d3478b Remove icon extraction from WinPE files. Add migration from old extracted icons to new media entries. 2023-11-03 01:24:27 -05:00
Pat Hartl f0c8296b6e Don't grab icon from old icon route 2023-11-03 00:10:13 -05:00
Pat Hartl 37f9027a80 Show server address and add disconnect button in Playnite addon settings 2023-11-03 00:07:20 -05:00
Pat Hartl 14a92bdc3e Merge branch 'media' 2023-11-02 23:54:15 -05:00
Pat Hartl 739453c8bc Pull media for Playnite from LANCommander server if it exists 2023-11-02 23:37:10 -05:00
Pat Hartl 82886221fc Add manual upload button for media 2023-11-02 23:18:19 -05:00
Pat Hartl 0fc8d756b3 Remove committed media. Store and serve proper mime type. 2023-11-02 22:13:19 -05:00
Pat Hartl 262e8cd468 Fix stretching of some media preview in editor 2023-11-02 19:30:30 -05:00
Pat Hartl d51eab151a Allow media grabber dialog to show results from multiple groups. Show media type in media grabber dialog. Allow modification of search for media grabber dialog. 2023-11-02 19:27:50 -05:00
Pat Hartl 499b0c910a Add ability to add media to games. Search media from steamgriddb.com 2023-11-02 01:24:42 -05:00
Pat Hartl 8c61a7e3b5 Allow customization of the address that gets broadcast by the beacon 2023-10-30 18:45:11 -05:00
Pat Hartl fd3f6c24b1
Update installermanifest.yaml 2023-10-28 14:44:41 -05:00
Pat Hartl fa57cc21ad Require administrator for redistributables 2023-10-28 14:20:41 -05:00
Pat Hartl 5a99f58f81 Run install script even if there is no archive for redistributables 2023-10-28 14:08:13 -05:00
Pat Hartl b23df9b2ad Support Playnite URI for connecting to server
e.g. playnite://lancommander/connect/http%3A%2F%2Flocalhost%3A1337
2023-10-28 13:56:54 -05:00
Pat Hartl ff9ec5a17b Merge branch 'redistributables' 2023-10-27 18:54:50 -05:00
Pat Hartl 06188d5800 Rename temp scripts to .ps1
PowerShell won't execute scripts that don't end in .ps1
2023-10-27 18:54:19 -05:00
Pat Hartl 5e47d8fa3d Remove display attributes 2023-10-24 19:13:09 -05:00
Pat Hartl 4dfed69a91 Fix detection result check 2023-10-24 19:12:59 -05:00
Pat Hartl b4405d3034 Added redistributable installing to plugin 2023-10-24 19:11:50 -05:00
Pat Hartl e4cf2aabfe Add new detect install script type, restrict script editor to specific types 2023-10-24 18:05:13 -05:00
Pat Hartl af265e9d34 Add archive menu item for redistributables 2023-10-22 18:12:01 -05:00
Pat Hartl 9e1b8ad7e3 Allow games to select redistributables required 2023-10-22 17:59:00 -05:00
Pat Hartl 9327511245 Add page to add/edit redistributables 2023-10-18 18:18:04 -05:00
Pat Hartl e3a08bc9c3 Added redistributable support to archives 2023-10-18 18:17:47 -05:00
Pat Hartl a3b1dabbb1 Add redistributable support to script editor 2023-10-18 18:11:42 -05:00
Pat Hartl 2287ac9922 Refactored archive uploader to use binding instead of Game parameter
May fix issues where game metadata is wiped after uploading an archive.
2023-10-18 18:11:20 -05:00
Pat Hartl 539ddb2f2e Move archive editor 2023-10-18 17:59:59 -05:00
Pat Hartl a00d0b3b42 Add database models and migrations for redistributables 2023-10-18 01:14:31 -05:00
Pat Hartl 80bd7dc66c Introduce user alias for persisting name changes separate from username 2023-10-16 20:48:12 -05:00
Pat Hartl 4b7e72b343 Require authentication for profile pages 2023-10-16 20:17:59 -05:00
Pat Hartl 04b8d45af4 Only show approval button is approval is required 2023-09-30 16:39:17 -05:00
Pat Hartl 3d86a617a9 Right align profile buttons 2023-09-30 14:03:27 -05:00
Pat Hartl 05116a65fb Don't wrap action column for servers 2023-09-30 14:02:00 -05:00
Pat Hartl c477782ca8 Update to latest version of RegParserDotNet 2023-09-24 21:15:22 -05:00
Pat Hartl ae14ceb306
Update installermanifest.yaml 2023-09-21 19:27:02 -05:00
Pat Hartl 35684c0a54 Added ability to import .reg exports into scripts 2023-09-21 19:00:12 -05:00
Pat Hartl 8f853fab56 Add profile page for users to change username/email, download saves, change password. 2023-09-18 00:58:40 -05:00
Pat Hartl 07a41aeaf5 Add ability for admins to change user password
Ref #28
2023-09-17 17:26:48 -05:00
Pat Hartl f94e5f63a8 Attempt to speed up loading of dashboard
Ref #29
2023-09-17 17:01:17 -05:00
Pat Hartl b32451d216 Add the ability to cancel installs in Playnite 2023-09-16 15:13:55 -05:00
Pat Hartl e00aa069fa Update installer manifest 2023-09-15 17:40:05 -05:00
Pat Hartl f376056eeb Only log LANCommander events 2023-09-15 17:38:44 -05:00
Pat Hartl 35ca8391c6 Don't lock user into validation loop if they close the auth window 2023-09-15 17:36:45 -05:00
Pat Hartl 54b7fec96b Remove auth check on Playnite start 2023-09-15 17:35:44 -05:00
Pat Hartl ebcb943e36 Remove non-working link 2023-09-15 17:35:25 -05:00
Pat Hartl 0c7b6f3b56 Add installer manifest for Playnite addon database 2023-09-15 17:13:10 -05:00
Pat Hartl 006bfcb866 Move away from top level statements to keep everything under LANCommander namespace 2023-09-14 17:44:16 -05:00
Pat Hartl 485b6febbe Fix token secret if it's shorter than 16 characters 2023-09-14 17:23:16 -05:00
Pat Hartl 75de5465c7 Better logging for auth controller 2023-09-14 17:14:03 -05:00
Pat Hartl 0a6b7fbe9d Update packages 2023-09-14 00:03:08 -05:00
Pat Hartl a2f9a8f217 Add ability to enable HTTP routes for servers to host static files 2023-09-13 23:29:59 -05:00
Pat Hartl dcaa9a8ac9 Fix settings nav highlights 2023-09-13 19:34:07 -05:00
Pat Hartl 4d7d0df9c0 Allow user saves storage path and max size to be configurable in settings 2023-09-13 19:32:36 -05:00
Pat Hartl 4476fbed5f Get archive path from storage path on settings 2023-09-12 20:23:06 -05:00
Pat Hartl 026efd586a Add ability to specify storage path for archives 2023-09-12 19:41:25 -05:00
Pat Hartl c725afc362 Add settings to control patching functionality 2023-09-12 19:24:04 -05:00
Pat Hartl 8a6c0b493b Fix non-Blazor checkboxes and remove jQuery 2023-09-12 18:02:30 -05:00
Pat Hartl 0f7124f205 Default token secret to new GUID 2023-09-12 17:32:17 -05:00
Pat Hartl d68e1d0433 Require admin for file manager 2023-09-11 20:40:30 -05:00
Pat Hartl 3647418004 Remove old archive browser 2023-09-11 20:23:36 -05:00
Pat Hartl 7e4aa07138 Switch to server-only rendering mode for stability and speed 2023-09-11 20:22:05 -05:00
Pat Hartl 9a55dac778 Auto populate server working directory based on executable path 2023-09-11 20:21:40 -05:00
Pat Hartl d869a5a763 Merge branch 'file-manager' 2023-09-11 19:40:17 -05:00
Pat Hartl 4d1fc67e55 Add file manager to main nav 2023-09-11 19:38:07 -05:00
Pat Hartl f49bec90f7 Simplify root path 2023-09-11 19:37:56 -05:00
Pat Hartl 8910746e1e Fix population of past for navigation 2023-09-11 19:37:45 -05:00
Pat Hartl 0354782e9f Change root path of file browser 2023-09-11 19:33:07 -05:00
Pat Hartl 606aefc957 Fix tree and breadcrumb hover in dark theme 2023-09-11 19:26:58 -05:00
Pat Hartl 4bf72b3bc8 Fix breadcrumbs when navigating using entries 2023-09-11 19:26:33 -05:00
Pat Hartl 857362181e Allow customization of OK button text 2023-09-11 19:18:03 -05:00
Pat Hartl f80e88a9a2 Switch server file inputs to new file picker, default root to app's path root 2023-09-11 19:17:49 -05:00
Pat Hartl 9e0768a96d Allow passing of func for selectable and visible entries 2023-09-11 19:17:00 -05:00
Pat Hartl 804e75da55 Hide selector based on func 2023-09-11 19:16:15 -05:00
Pat Hartl 2cf91962a1 Fix dark mode file manager dialog 2023-09-11 17:58:16 -05:00
Pat Hartl fda3f01847 Use file picker input on games 2023-09-11 17:57:40 -05:00
Pat Hartl d54bf3c4e8 Avoid errors on file system when populating entries 2023-09-11 17:57:24 -05:00
Pat Hartl bf63813e90 Rename input to file picker 2023-09-11 17:56:23 -05:00
Pat Hartl 8de8d7cfdc Fix selecting file in archive file picker 2023-09-11 02:06:59 -05:00
Pat Hartl 3b7a0db74e Switch archive file inputs to use new file manager 2023-09-11 01:56:50 -05:00
Pat Hartl d3f13c6493 Set archive root name to Root 2023-09-11 01:55:09 -05:00
Pat Hartl 046013491d Fix styling of body of file manager 2023-09-11 01:54:58 -05:00
Pat Hartl 38bec7c9aa Move file render population to initialization of component 2023-09-11 01:38:00 -05:00
Pat Hartl 15ad1d35a9 Fix potential null references 2023-09-11 01:37:37 -05:00
Pat Hartl a9149db849 Merge branch 'main' into file-manager 2023-09-11 00:20:45 -05:00
Pat Hartl 2e792b74aa Don't allow new folder / deletes in archives 2023-09-10 23:46:48 -05:00
Pat Hartl 41d1ff4c5b Prevent tree expansion blocking render 2023-09-10 23:45:25 -05:00
Pat Hartl a6417f4afa Fix alerts blocking refresh 2023-09-10 23:42:00 -05:00
Pat Hartl cfe6eafc19 Get current entries async using the thread pool 2023-09-10 23:40:15 -05:00
Pat Hartl 3f7a47687c Correctly filter current files listed from archive directory 2023-09-10 23:29:23 -05:00
Pat Hartl 8d399d11e6 Fix child population of archive directories 2023-09-10 23:28:51 -05:00
Pat Hartl 63df396fad Dynamically get file system entries on tree expand only if we're of source FileSystem 2023-09-10 23:28:33 -05:00
Pat Hartl ca1520341c Avoid double file system enumerations 2023-09-10 20:03:46 -05:00
Pat Hartl 1ac8513f95 Add predicate for filtering selectable and visible entries in file manager. Start to fix support for archives. 2023-09-10 20:02:33 -05:00
Pat Hartl acddc47a89 Use flags to dictate which features a file manager has 2023-09-10 18:32:50 -05:00
Pat Hartl c12b834571 Added pop confirm for deleting files 2023-09-10 17:30:26 -05:00
Pat Hartl bc35e2337e Utilize ChangeDirectory on nav controls 2023-09-10 17:13:26 -05:00
Pat Hartl a7ddfb8ae6 Don't add path to history if it's just a refresh 2023-09-10 17:13:10 -05:00
Pat Hartl 8475a683dd Disable nav buttons if there's no history 2023-09-10 17:12:32 -05:00
Pat Hartl 3525ce96da Force state change on refresh 2023-09-10 17:12:01 -05:00
Pat Hartl d7c2ae0d85 Add logger to upload controller 2023-09-10 17:11:30 -05:00
Pat Hartl 749d07a546 Add ability to upload files 2023-09-10 17:11:06 -05:00
Pat Hartl 475a035d1c Trigger state change when directory changes 2023-09-10 02:28:14 -05:00
Pat Hartl 0441f6a3da New folder creation. Allow deletion of selected items. 2023-09-10 02:19:11 -05:00
Pat Hartl bd5f1a16ec Add selection to file table 2023-09-10 02:16:53 -05:00
Pat Hartl 81bf3265db Styling for breadcrumbs. Manipulation buttons. Allow archive ID to load archive from system. Populate file/directory info. 2023-09-10 00:01:52 -05:00
Pat Hartl 4299d3b296 Register IPX relay singleton 2023-09-09 17:36:38 -05:00
Pat Hartl 235d50a73a WIP file manager 2023-09-09 17:36:20 -05:00
Pat Hartl 329c147419 Added built-in IPX relay for DOSBox 2023-09-08 22:26:28 -05:00
Pat Hartl 6483bea96b Defaults in settings model. Allow force load of settings from file 2023-09-08 22:25:12 -05:00
Pat Hartl f474f4fee2 Added support for dark theme 2023-09-05 19:41:11 -05:00
Pat Hartl 2d3c3c7407
Update LANCommander.Release.yml 2023-09-04 13:41:32 -05:00
251 changed files with 35393 additions and 2484 deletions

View File

@ -40,7 +40,7 @@ jobs:
- name: Upload Artifacts
uses: actions/upload-artifact@v2
with:
name: LANCommander-${{ steps.trim_tag_ref.outputs.replaced }}
name: LANCommander-v${{ steps.trim_tag_ref.outputs.replaced }}
path: "./_Build"
# Client
- uses: actions/checkout@v3

2
.gitignore vendored
View File

@ -352,3 +352,5 @@ Upload/
LANCommander/Icon/
LANCommander/Settings.yml
LANCommander/Saves/
LANCommander/Media/
LANCommander/Uploads/

View File

@ -5,8 +5,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.52" />
<PackageReference Include="System.Text.Json" Version="5.0.1" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.53" />
<PackageReference Include="System.Text.Json" Version="7.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -1,17 +1,14 @@
using LANCommander.PlaynitePlugin.Helpers;
using LANCommander.SDK.Enums;
using LANCommander.SDK.Extensions;
using LANCommander.SDK;
using LANCommander.SDK.Helpers;
using LANCommander.SDK.Models;
using LANCommander.SDK.PowerShell;
using Playnite.SDK;
using Playnite.SDK.Models;
using Playnite.SDK.Plugins;
using SharpCompress.Common;
using SharpCompress.Readers;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace LANCommander.PlaynitePlugin
{
@ -20,15 +17,11 @@ namespace LANCommander.PlaynitePlugin
public static readonly ILogger Logger = LogManager.GetLogger();
private LANCommanderLibraryPlugin Plugin;
private PowerShellRuntime PowerShellRuntime;
private Playnite.SDK.Models.Game PlayniteGame;
public LANCommanderInstallController(LANCommanderLibraryPlugin plugin, Playnite.SDK.Models.Game game) : base(game)
{
Name = "Install using LANCommander";
Plugin = plugin;
PlayniteGame = game;
PowerShellRuntime = new PowerShellRuntime();
}
public override void Install(InstallActionArgs args)
@ -43,238 +36,176 @@ namespace LANCommander.PlaynitePlugin
}
var gameId = Guid.Parse(Game.GameId);
var game = Plugin.LANCommander.GetGame(gameId);
Logger.Trace($"Installing game {game.Title} ({game.Id})...");
string installDirectory = null;
var installDirectory = RetryHelper.RetryOnException(10, TimeSpan.FromMilliseconds(500), "", () =>
var result = Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress =>
{
Logger.Trace("Attempting to download and extract game...");
return DownloadAndExtract(game);
});
var gameManager = new GameManager(Plugin.LANCommanderClient, Plugin.Settings.InstallDirectory);
if (installDirectory == "")
throw new Exception("Could not extract the install archive. Retry the install or check your connection.");
Stopwatch stopwatch = new Stopwatch();
var installInfo = new GameInstallationData()
{
InstallDirectory = installDirectory
};
stopwatch.Start();
PlayniteGame.InstallDirectory = installDirectory;
var lastTotalSize = 0d;
var speed = 0d;
SDK.GameManifest manifest = null;
var writeManifestSuccess = RetryHelper.RetryOnException(10, TimeSpan.FromSeconds(1), false, () =>
{
Logger.Trace("Attempting to get game manifest...");
manifest = Plugin.LANCommander.GetGameManifest(gameId);
WriteManifest(manifest, installDirectory);
return true;
});
if (!writeManifestSuccess)
throw new Exception("Could not get or write the manifest file. Retry the install or check your connection.");
Logger.Trace("Saving scripts...");
SaveScript(game, installDirectory, ScriptType.Install);
SaveScript(game, installDirectory, ScriptType.Uninstall);
SaveScript(game, installDirectory, ScriptType.NameChange);
SaveScript(game, installDirectory, ScriptType.KeyChange);
try
{
PowerShellRuntime.RunScript(PlayniteGame, ScriptType.Install);
PowerShellRuntime.RunScript(PlayniteGame, ScriptType.NameChange, Plugin.Settings.PlayerName);
var key = Plugin.LANCommander.GetAllocatedKey(game.Id);
PowerShellRuntime.RunScript(PlayniteGame, ScriptType.KeyChange, $"\"{key}\"");
}
catch { }
Plugin.UpdateGame(manifest, gameId);
Plugin.DownloadCache.Remove(gameId);
InvokeOnInstalled(new GameInstalledEventArgs(installInfo));
}
private string DownloadAndExtract(LANCommander.SDK.Models.Game game)
{
if (game == null)
{
Logger.Trace("Game failed to download! No game was specified!");
throw new Exception("Game failed to download!");
}
var destination = Path.Combine(Plugin.Settings.InstallDirectory, game.Title.SanitizeFilename());
Logger.Trace($"Downloading and extracting \"{game.Title}\" to path {destination}");
Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress =>
{
try
gameManager.OnArchiveExtractionProgress += (long pos, long len) =>
{
Directory.CreateDirectory(destination);
progress.ProgressMaxValue = 100;
progress.CurrentProgressValue = 0;
using (var gameStream = Plugin.LANCommander.StreamGame(game.Id))
using (var reader = ReaderFactory.Open(gameStream))
if (stopwatch.ElapsedMilliseconds > 500)
{
progress.ProgressMaxValue = gameStream.Length;
var percent = Math.Ceiling((pos / (decimal)len) * 100);
gameStream.OnProgress += (pos, len) =>
{
progress.CurrentProgressValue = pos;
};
progress.ProgressMaxValue = len;
progress.CurrentProgressValue = pos;
reader.WriteAllToDirectory(destination, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true
});
speed = (double)(progress.CurrentProgressValue - lastTotalSize) / (stopwatch.ElapsedMilliseconds / 1000d);
progress.Text = $"Downloading {Game.Name} ({percent}%) | {ByteSizeLib.ByteSize.FromBytes(speed).ToString("#.#")}/s";
lastTotalSize = pos;
stopwatch.Restart();
}
}
catch (Exception ex)
};
gameManager.OnArchiveEntryExtractionProgress += (object sender, ArchiveEntryExtractionProgressArgs e) =>
{
Logger.Error(ex, $"Could not extract to path {destination}");
if (Directory.Exists(destination))
if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested)
{
Logger.Trace("Cleaning up orphaned install files after bad install...");
gameManager.CancelInstall();
Directory.Delete(destination, true);
progress.IsIndeterminate = true;
}
};
throw new Exception("The game archive could not be extracted. Please try again or fix the archive!");
}
installDirectory = gameManager.Install(gameId);
stopwatch.Stop();
},
new GlobalProgressOptions($"Downloading {game.Title}...")
new GlobalProgressOptions($"Preparing to download {Game.Name}")
{
IsIndeterminate = false,
Cancelable = false,
Cancelable = true,
});
Logger.Trace($"Game successfully downloaded and extracted to {destination}");
// Install any redistributables
var game = Plugin.LANCommanderClient.GetGame(gameId);
return destination;
}
private string Download(LANCommander.SDK.Models.Game game)
{
string tempFile = String.Empty;
if (game != null)
if (game.Redistributables != null && game.Redistributables.Count() > 0)
{
Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress =>
{
progress.ProgressMaxValue = 100;
progress.CurrentProgressValue = 0;
var redistributableManager = new RedistributableManager(Plugin.LANCommanderClient);
var destination = Plugin.LANCommander.DownloadGame(game.Id, (changed) =>
{
progress.CurrentProgressValue = changed.ProgressPercentage;
}, (complete) =>
{
progress.CurrentProgressValue = 100;
});
// Lock the thread until download is done
while (progress.CurrentProgressValue != 100)
{
}
tempFile = destination;
redistributableManager.Install(game);
},
new GlobalProgressOptions($"Downloading {game.Title}...")
new GlobalProgressOptions("Installing redistributables...")
{
IsIndeterminate = false,
IsIndeterminate = true,
Cancelable = false,
});
return tempFile;
}
else
throw new Exception("Game failed to download!");
}
private string Extract(LANCommander.SDK.Models.Game game, string archivePath)
{
var destination = Path.Combine(Plugin.Settings.InstallDirectory, game.Title.SanitizeFilename());
Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress =>
if (!result.Canceled && result.Error == null && !String.IsNullOrWhiteSpace(installDirectory))
{
Directory.CreateDirectory(destination);
var manifest = ManifestHelper.Read(installDirectory);
using (var fs = File.OpenRead(archivePath))
using (var ts = new TrackableStream(fs))
using (var reader = ReaderFactory.Open(ts))
Plugin.UpdateGame(manifest);
var installInfo = new GameInstallationData
{
progress.ProgressMaxValue = ts.Length;
ts.OnProgress += (pos, len) =>
{
progress.CurrentProgressValue = pos;
};
InstallDirectory = installDirectory,
};
reader.WriteAllToDirectory(destination, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true
});
}
},
new GlobalProgressOptions($"Extracting {game.Title}...")
RunInstallScript(installDirectory);
RunNameChangeScript(installDirectory);
RunKeyChangeScript(installDirectory);
InvokeOnInstalled(new GameInstalledEventArgs(installInfo));
}
else if (result.Canceled)
{
IsIndeterminate = false,
Cancelable = false,
});
var dbGame = Plugin.PlayniteApi.Database.Games.Get(Game.Id);
return destination;
dbGame.IsInstalling = false;
dbGame.IsInstalled = false;
Plugin.PlayniteApi.Database.Games.Update(dbGame);
}
else if (result.Error != null)
throw result.Error;
}
private void WriteManifest(SDK.GameManifest manifest, string installDirectory)
private int RunInstallScript(string installDirectory)
{
var destination = Path.Combine(installDirectory, "_manifest.yml");
var manifest = ManifestHelper.Read(installDirectory);
var path = ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.Install);
Logger.Trace($"Attempting to write manifest to path {destination}");
if (File.Exists(path))
{
var script = new PowerShellScript();
var serializer = new SerializerBuilder()
.WithNamingConvention(new PascalCaseNamingConvention())
.Build();
script.AddVariable("InstallDirectory", installDirectory);
script.AddVariable("GameManifest", manifest);
script.AddVariable("DefaultInstallDirectory", Plugin.Settings.InstallDirectory);
script.AddVariable("ServerAddress", Plugin.Settings.ServerAddress);
Logger.Trace("Serializing manifest...");
var yaml = serializer.Serialize(manifest);
script.UseFile(ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.Install));
Logger.Trace("Writing manifest file...");
File.WriteAllText(destination, yaml);
return script.Execute();
}
return 0;
}
private void SaveScript(LANCommander.SDK.Models.Game game, string installationDirectory, ScriptType type)
private int RunNameChangeScript(string installDirectory)
{
var script = game.Scripts.FirstOrDefault(s => s.Type == type);
var manifest = ManifestHelper.Read(installDirectory);
var path = ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.NameChange);
if (script == null)
return;
if (File.Exists(path))
{
var script = new PowerShellScript();
if (script.RequiresAdmin)
script.Contents = "# Requires Admin" + "\r\n\r\n" + script.Contents;
script.AddVariable("InstallDirectory", installDirectory);
script.AddVariable("GameManifest", manifest);
script.AddVariable("DefaultInstallDirectory", Plugin.Settings.InstallDirectory);
script.AddVariable("ServerAddress", Plugin.Settings.ServerAddress);
script.AddVariable("OldPlayerAlias", "");
script.AddVariable("NewPlayerAlias", Plugin.Settings.PlayerName);
var filename = PowerShellRuntime.GetScriptFilePath(PlayniteGame, type);
script.UseFile(path);
if (File.Exists(filename))
File.Delete(filename);
return script.Execute();
}
Logger.Trace($"Writing {type} script to {filename}");
return 0;
}
File.WriteAllText(filename, script.Contents);
private int RunKeyChangeScript(string installDirectory)
{
var manifest = ManifestHelper.Read(installDirectory);
var path = ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.KeyChange);
if (File.Exists(path))
{
var script = new PowerShellScript();
var key = Plugin.LANCommanderClient.GetAllocatedKey(manifest.Id);
script.AddVariable("InstallDirectory", installDirectory);
script.AddVariable("GameManifest", manifest);
script.AddVariable("DefaultInstallDirectory", Plugin.Settings.InstallDirectory);
script.AddVariable("ServerAddress", Plugin.Settings.ServerAddress);
script.AddVariable("AllocatedKey", key);
script.UseFile(path);
return script.Execute();
}
return 0;
}
}
}

View File

@ -12,6 +12,7 @@
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@ -34,19 +35,28 @@
<Reference Include="BeaconLib, Version=1.0.2.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\rix0rrr.BeaconLib.1.0.2\lib\net40\BeaconLib.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Bcl.AsyncInterfaces, Version=5.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Bcl.AsyncInterfaces.5.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll</HintPath>
<Reference Include="ByteSize, Version=2.1.1.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\ByteSize.2.1.1\lib\net45\ByteSize.dll</HintPath>
</Reference>
<Reference Include="Playnite.SDK, Version=6.9.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\PlayniteSDK.6.9.0\lib\net462\Playnite.SDK.dll</HintPath>
<Reference Include="Microsoft.Bcl.AsyncInterfaces, Version=8.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Bcl.AsyncInterfaces.8.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.8.0.0\lib\net462\Microsoft.Extensions.DependencyInjection.Abstractions.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Extensions.Logging.Abstractions, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Extensions.Logging.Abstractions.8.0.0\lib\net462\Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
</Reference>
<Reference Include="Playnite.SDK, Version=6.10.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\PlayniteSDK.6.10.0\lib\net462\Playnite.SDK.dll</HintPath>
</Reference>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="RestSharp, Version=106.15.0.0, Culture=neutral, PublicKeyToken=598062e77f915f75, processorArchitecture=MSIL">
<HintPath>..\packages\RestSharp.106.15.0\lib\net452\RestSharp.dll</HintPath>
</Reference>
<Reference Include="SharpCompress, Version=0.33.0.0, Culture=neutral, PublicKeyToken=afb0a02973931d96, processorArchitecture=MSIL">
<HintPath>..\packages\SharpCompress.0.33.0\lib\net462\SharpCompress.dll</HintPath>
<Reference Include="SharpCompress, Version=0.34.2.0, Culture=neutral, PublicKeyToken=afb0a02973931d96, processorArchitecture=MSIL">
<HintPath>..\packages\SharpCompress.0.34.2\lib\net462\SharpCompress.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
@ -67,14 +77,14 @@
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
</Reference>
<Reference Include="System.Text.Encoding.CodePages, Version=7.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Text.Encoding.CodePages.7.0.0\lib\net462\System.Text.Encoding.CodePages.dll</HintPath>
<Reference Include="System.Text.Encoding.CodePages, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Text.Encoding.CodePages.8.0.0\lib\net462\System.Text.Encoding.CodePages.dll</HintPath>
</Reference>
<Reference Include="System.Text.Encodings.Web, Version=5.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Text.Encodings.Web.5.0.0\lib\net461\System.Text.Encodings.Web.dll</HintPath>
<Reference Include="System.Text.Encodings.Web, Version=8.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Text.Encodings.Web.8.0.0\lib\net462\System.Text.Encodings.Web.dll</HintPath>
</Reference>
<Reference Include="System.Text.Json, Version=5.0.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Text.Json.5.0.1\lib\net461\System.Text.Json.dll</HintPath>
<Reference Include="System.Text.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Text.Json.8.0.0\lib\net462\System.Text.Json.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll</HintPath>
@ -93,18 +103,18 @@
<Reference Include="WindowsBase" />
<Reference Include="YamlDotNet, Version=5.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\YamlDotNet.5.4.0\lib\net45\YamlDotNet.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="ZstdSharp, Version=0.7.2.0, Culture=neutral, PublicKeyToken=8d151af33a4ad5cf, processorArchitecture=MSIL">
<HintPath>..\packages\ZstdSharp.Port.0.7.2\lib\net461\ZstdSharp.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="TrackableStream.cs" />
<Compile Include="Extensions\MultiplayerInfoExtensions.cs" />
<Compile Include="Helpers\RetryHelper.cs" />
<Compile Include="PowerShellRuntime.cs" />
<Compile Include="Services\GameSaveService.cs" />
<Compile Include="PlayniteLogger.cs" />
<Compile Include="SaveController.cs" />
<Compile Include="UninstallController.cs" />
<Compile Include="InstallController.cs" />
<Compile Include="LANCommanderClient.cs" />
<Compile Include="LANCommanderLibraryClient.cs" />
<Compile Include="LANCommanderLibraryPlugin.cs" />
<Compile Include="ViewModels\LANCommanderSettingsViewModel.cs" />
<Compile Include="Views\LANCommanderSettingsView.xaml.cs">
@ -145,6 +155,10 @@
</Page>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LANCommander.PowerShell\LANCommander.PowerShell.csproj">
<Project>{807943bf-0c7d-4ed3-8393-cfee64e3138c}</Project>
<Name>LANCommander.PowerShell</Name>
</ProjectReference>
<ProjectReference Include="..\LANCommander.SDK\LANCommander.SDK.csproj">
<Project>{4c2a71fd-a30b-4d62-888a-4ef843d8e506}</Project>
<Name>LANCommander.SDK</Name>

View File

@ -1,20 +0,0 @@
using Playnite.SDK;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LANCommander.PlaynitePlugin
{
public class LANCommanderLibraryClient : LibraryClient
{
public override bool IsInstalled => true;
public override void Open()
{
System.Diagnostics.Process.Start("https://localhost:7087");
}
}
}

View File

@ -1,5 +1,6 @@
using LANCommander.PlaynitePlugin.Extensions;
using LANCommander.PlaynitePlugin.Services;
using LANCommander.SDK.Helpers;
using LANCommander.SDK.PowerShell;
using Playnite.SDK;
using Playnite.SDK.Events;
using Playnite.SDK.Models;
@ -9,6 +10,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Web;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
@ -20,13 +22,11 @@ namespace LANCommander.PlaynitePlugin
{
public static readonly ILogger Logger = LogManager.GetLogger();
internal LANCommanderSettingsViewModel Settings { get; set; }
internal LANCommanderClient LANCommander { get; set; }
internal PowerShellRuntime PowerShellRuntime { get; set; }
internal GameSaveService GameSaveService { get; set; }
internal LANCommander.SDK.Client LANCommanderClient { get; set; }
internal LANCommanderSaveController SaveController { get; set; }
public override Guid Id { get; } = Guid.Parse("48e1bac7-e0a0-45d7-ba83-36f5e9e959fc");
public override string Name => "LANCommander";
public override LibraryClient Client { get; } = new LANCommanderLibraryClient();
internal Dictionary<Guid, string> DownloadCache = new Dictionary<Guid, string>();
@ -39,16 +39,14 @@ namespace LANCommander.PlaynitePlugin
Settings = new LANCommanderSettingsViewModel(this);
LANCommander = new LANCommanderClient(Settings.ServerAddress);
LANCommander.Token = new SDK.Models.AuthToken()
LANCommanderClient = new SDK.Client(Settings.ServerAddress, new PlayniteLogger(Logger));
LANCommanderClient.UseToken(new SDK.Models.AuthToken()
{
AccessToken = Settings.AccessToken,
RefreshToken = Settings.RefreshToken,
};
});
PowerShellRuntime = new PowerShellRuntime();
GameSaveService = new GameSaveService(LANCommander, PlayniteApi, PowerShellRuntime);
// GameSaveService = new GameSaveService(LANCommander, PlayniteApi, PowerShellRuntime);
api.UriHandler.RegisterSource("lancommander", args =>
{
@ -74,38 +72,46 @@ namespace LANCommander.PlaynitePlugin
if (Guid.TryParse(args.Arguments[1], out gameId))
PlayniteApi.StartGame(gameId);
break;
case "connect":
if (args.Arguments.Length == 1)
{
ShowAuthenticationWindow();
break;
}
ShowAuthenticationWindow(HttpUtility.UrlDecode(args.Arguments[1]));
break;
}
});
}
public override void OnApplicationStarted(OnApplicationStartedEventArgs args)
{
if (LANCommander.Token == null || LANCommander.Client == null || !LANCommander.ValidateToken(LANCommander.Token))
{
Logger.Trace("No valid authentication token exists. Showing auth window...");
ShowAuthenticationWindow();
}
}
public bool ValidateConnection()
{
return LANCommander.ValidateToken(LANCommander.Token);
return LANCommanderClient.ValidateToken();
}
public override IEnumerable<GameMetadata> GetGames(LibraryGetGamesArgs args)
{
var gameMetadata = new List<GameMetadata>();
while (!ValidateConnection())
if (!ValidateConnection())
{
Logger.Trace("Authentication invalid, showing auth window...");
ShowAuthenticationWindow();
if (!ValidateConnection())
{
Logger.Trace("User cancelled authentication.");
throw new Exception("You must set up a valid connection to a LANCommander server.");
}
}
var games = LANCommander
var games = LANCommanderClient
.GetGames()
.Where(g => g.Archives != null && g.Archives.Count() > 0);
.Where(g => g != null && g.Archives != null && g.Archives.Count() > 0);
foreach (var game in games)
{
@ -113,7 +119,7 @@ namespace LANCommander.PlaynitePlugin
{
Logger.Trace($"Importing/updating metadata for game \"{game.Title}\"...");
var manifest = LANCommander.GetGameManifest(game.Id);
var manifest = LANCommanderClient.GetGameManifest(game.Id);
Logger.Trace("Successfully grabbed game manifest");
var existingGame = PlayniteApi.Database.Games.FirstOrDefault(g => g.GameId == game.Id.ToString() && g.PluginId == Id && g.IsInstalled);
@ -122,7 +128,7 @@ namespace LANCommander.PlaynitePlugin
{
Logger.Trace("Game already exists in library, updating metadata...");
UpdateGame(manifest, game.Id);
UpdateGame(manifest);
continue;
}
@ -138,7 +144,6 @@ namespace LANCommander.PlaynitePlugin
GameId = game.Id.ToString(),
ReleaseDate = new ReleaseDate(manifest.ReleasedOn),
//Version = game.Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault().Version,
Icon = new MetadataFile($"{Settings.ServerAddress}{manifest.Icon}"),
GameActions = game.Actions.OrderBy(a => a.SortOrder).Select(a => new PN.SDK.Models.GameAction()
{
Name = a.Name,
@ -175,6 +180,15 @@ namespace LANCommander.PlaynitePlugin
if (manifest.OnlineMultiplayer != null)
metadata.Features.Add(new MetadataNameProperty($"Online Multiplayer {manifest.OnlineMultiplayer.GetPlayerCount()}".Trim()));
if (game.Media.Any(m => m.Type == SDK.Enums.MediaType.Icon))
metadata.Icon = new MetadataFile(LANCommanderClient.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Icon)));
if (game.Media.Any(m => m.Type == SDK.Enums.MediaType.Cover))
metadata.CoverImage = new MetadataFile(LANCommanderClient.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Cover)));
if (game.Media.Any(m => m.Type == SDK.Enums.MediaType.Background))
metadata.BackgroundImage = new MetadataFile(LANCommanderClient.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Background)));
gameMetadata.Add(metadata);
}
catch (Exception ex)
@ -208,9 +222,9 @@ namespace LANCommander.PlaynitePlugin
if (args.Games.Count == 1 && args.Games.First().IsInstalled && !String.IsNullOrWhiteSpace(args.Games.First().InstallDirectory))
{
var nameChangeScriptPath = PowerShellRuntime.GetScriptFilePath(args.Games.First(), SDK.Enums.ScriptType.NameChange);
var keyChangeScriptPath = PowerShellRuntime.GetScriptFilePath(args.Games.First(), SDK.Enums.ScriptType.KeyChange);
var installScriptPath = PowerShellRuntime.GetScriptFilePath(args.Games.First(), SDK.Enums.ScriptType.Install);
var nameChangeScriptPath = ScriptHelper.GetScriptFilePath(args.Games.First().InstallDirectory, SDK.Enums.ScriptType.NameChange);
var keyChangeScriptPath = ScriptHelper.GetScriptFilePath(args.Games.First().InstallDirectory, SDK.Enums.ScriptType.KeyChange);
var installScriptPath = ScriptHelper.GetScriptFilePath(args.Games.First().InstallDirectory, SDK.Enums.ScriptType.Install);
if (File.Exists(nameChangeScriptPath))
{
@ -226,7 +240,13 @@ namespace LANCommander.PlaynitePlugin
var result = PlayniteApi.Dialogs.SelectString("Enter your player name", "Change Player Name", Settings.PlayerName);
if (result.Result == true)
PowerShellRuntime.RunScript(nameChangeArgs.Games.First(), SDK.Enums.ScriptType.NameChange, $@"""{result.SelectedString}"" ""{oldName}""");
{
var game = nameChangeArgs.Games.First();
RunNameChangeScript(game.InstallDirectory, oldName, result.SelectedString);
LANCommanderClient.ChangeAlias(result.SelectedString);
}
}
};
}
@ -245,12 +265,12 @@ namespace LANCommander.PlaynitePlugin
if (Guid.TryParse(keyChangeArgs.Games.First().GameId, out gameId))
{
// NUKIEEEE
var newKey = LANCommander.GetNewKey(gameId);
var newKey = LANCommanderClient.GetNewKey(gameId);
if (String.IsNullOrEmpty(newKey))
PlayniteApi.Dialogs.ShowErrorMessage("There are no more keys available on the server.", "No Keys Available");
else
PowerShellRuntime.RunScript(keyChangeArgs.Games.First(), SDK.Enums.ScriptType.KeyChange, $@"""{newKey}""");
RunKeyChangeScript(keyChangeArgs.Games.First().InstallDirectory, newKey);
}
else
{
@ -272,13 +292,9 @@ namespace LANCommander.PlaynitePlugin
Guid gameId;
if (Guid.TryParse(installArgs.Games.First().GameId, out gameId))
{
PowerShellRuntime.RunScript(installArgs.Games.First(), SDK.Enums.ScriptType.Install);
}
RunInstallScript(installArgs.Games.First().InstallDirectory);
else
{
PlayniteApi.Dialogs.ShowErrorMessage("This game could not be found on the server. Your game may be corrupted.");
}
}
};
}
@ -315,12 +331,42 @@ namespace LANCommander.PlaynitePlugin
public override void OnGameStarting(OnGameStartingEventArgs args)
{
GameSaveService.DownloadSave(args.Game);
if (args.Game.PluginId == Id)
{
var gameId = Guid.Parse(args.Game.GameId);
LANCommanderClient.StartPlaySession(gameId);
try
{
SaveController = new LANCommanderSaveController(this, args.Game);
SaveController.Download(args.Game);
}
catch (Exception ex)
{
Logger?.Error(ex, "Could not download save");
}
}
}
public override void OnGameStopped(OnGameStoppedEventArgs args)
{
GameSaveService.UploadSave(args.Game);
if (args.Game.PluginId == Id)
{
var gameId = Guid.Parse(args.Game.GameId);
LANCommanderClient.EndPlaySession(gameId);
try
{
SaveController = new LANCommanderSaveController(this, args.Game);
SaveController.Upload(args.Game);
}
catch (Exception ex)
{
Logger?.Error(ex, "Could not upload save");
}
}
}
public override IEnumerable<TopPanelItem> GetTopPanelItems()
@ -374,6 +420,8 @@ namespace LANCommander.PlaynitePlugin
}
else
{
var oldName = Settings.PlayerName;
Settings.PlayerName = result.SelectedString;
Logger.Trace($"New player name of \"{Settings.PlayerName}\" has been set!");
@ -383,16 +431,28 @@ namespace LANCommander.PlaynitePlugin
var games = PlayniteApi.Database.Games.Where(g => g.IsInstalled).ToList();
LANCommanderClient.ChangeAlias(result.SelectedString);
Logger.Trace($"Running name change scripts across {games.Count} installed game(s)");
PowerShellRuntime.RunScripts(games, SDK.Enums.ScriptType.NameChange, Settings.PlayerName);
foreach (var game in games)
{
var script = new PowerShellScript();
script.AddVariable("OldName", oldName);
script.AddVariable("NewName", Settings.PlayerName);
script.UseFile(ScriptHelper.GetScriptFilePath(game.InstallDirectory, SDK.Enums.ScriptType.NameChange));
script.Execute();
}
}
}
else
Logger.Trace("Name change was cancelled");
}
public Window ShowAuthenticationWindow()
public Window ShowAuthenticationWindow(string serverAddress = null, EventHandler onClose = null)
{
Window window = null;
Application.Current.Dispatcher.Invoke((Action)delegate
@ -408,21 +468,25 @@ namespace LANCommander.PlaynitePlugin
window.Content = new Views.Authentication(this);
window.DataContext = new ViewModels.Authentication()
{
ServerAddress = Settings?.ServerAddress
ServerAddress = serverAddress ?? Settings?.ServerAddress
};
window.Owner = PlayniteApi.Dialogs.GetCurrentAppWindow();
window.WindowStartupLocation = WindowStartupLocation.CenterOwner;
window.ResizeMode = ResizeMode.NoResize;
if (onClose != null)
window.Closed += onClose;
window.ShowDialog();
});
return window;
}
public void UpdateGame(SDK.GameManifest manifest, Guid gameId)
public void UpdateGame(SDK.GameManifest manifest)
{
var game = PlayniteApi.Database.Games.First(g => g.GameId == gameId.ToString());
var game = PlayniteApi.Database.Games.FirstOrDefault(g => g.GameId == manifest?.Id.ToString());
if (game == null)
return;
@ -432,8 +496,6 @@ namespace LANCommander.PlaynitePlugin
else
game.GameActions.Clear();
game.Icon = $"{Settings.ServerAddress}{manifest.Icon}";
if (manifest.Actions == null)
throw new Exception("The game has no actions defined.");
@ -506,5 +568,77 @@ namespace LANCommander.PlaynitePlugin
PlayniteApi.Database.Games.Update(game);
}
private int RunInstallScript(string installDirectory)
{
var manifest = ManifestHelper.Read(installDirectory);
var path = ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.Install);
if (File.Exists(path))
{
var script = new PowerShellScript();
script.AddVariable("InstallDirectory", installDirectory);
script.AddVariable("GameManifest", manifest);
script.AddVariable("DefaultInstallDirectory", Settings.InstallDirectory);
script.AddVariable("ServerAddress", Settings.ServerAddress);
script.UseFile(path);
return script.Execute();
}
return 0;
}
private int RunNameChangeScript(string installDirectory, string oldPlayerAlias, string newPlayerAlias)
{
var manifest = ManifestHelper.Read(installDirectory);
var path = ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.NameChange);
if (File.Exists(path))
{
var script = new PowerShellScript();
script.AddVariable("InstallDirectory", installDirectory);
script.AddVariable("GameManifest", manifest);
script.AddVariable("DefaultInstallDirectory", Settings.InstallDirectory);
script.AddVariable("ServerAddress", Settings.ServerAddress);
script.AddVariable("OldPlayerAlias", oldPlayerAlias);
script.AddVariable("NewPlayerAlias", newPlayerAlias);
script.UseFile(path);
return script.Execute();
}
return 0;
}
private int RunKeyChangeScript(string installDirectory, string key = "")
{
var manifest = ManifestHelper.Read(installDirectory);
var path = ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.KeyChange);
if (File.Exists(path))
{
var script = new PowerShellScript();
if (String.IsNullOrEmpty(key))
key = LANCommanderClient.GetAllocatedKey(manifest.Id);
script.AddVariable("InstallDirectory", installDirectory);
script.AddVariable("GameManifest", manifest);
script.AddVariable("DefaultInstallDirectory", Settings.InstallDirectory);
script.AddVariable("ServerAddress", Settings.ServerAddress);
script.AddVariable("AllocatedKey", key);
script.UseFile(path);
return script.Execute();
}
return 0;
}
}
}

View File

@ -0,0 +1,55 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LANCommander.PlaynitePlugin
{
public sealed class PlayniteLogger : ILogger
{
private readonly Playnite.SDK.ILogger Logger;
public PlayniteLogger(Playnite.SDK.ILogger logger) {
Logger = logger;
}
public IDisposable BeginScope<TState>(TState state)
{
return default;
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
switch (logLevel)
{
case LogLevel.Trace:
Logger?.Trace(formatter.Invoke(state, exception));
break;
case LogLevel.Debug:
Logger?.Debug(formatter.Invoke(state, exception));
break;
case LogLevel.Information:
Logger.Info(formatter.Invoke(state, exception));
break;
case LogLevel.Warning:
Logger.Warn(formatter.Invoke(state, exception));
break;
case LogLevel.Error:
case LogLevel.Critical:
Logger.Error(formatter.Invoke(state, exception));
break;
}
}
}
}

View File

@ -1,160 +0,0 @@
using LANCommander.SDK.Enums;
using Playnite.SDK;
using Playnite.SDK.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Runtime.InteropServices;
using System.Security.RightsManagement;
using System.Text;
using System.Threading.Tasks;
namespace LANCommander.PlaynitePlugin
{
internal class PowerShellRuntime
{
public static readonly ILogger Logger = LogManager.GetLogger();
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool Wow64DisableWow64FsRedirection(ref IntPtr ptr);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool Wow64RevertWow64FsRedirection(ref IntPtr ptr);
public void RunCommand(string command, bool asAdmin = false)
{
Logger.Trace($"Executing command `{command}` | Admin: {asAdmin}");
var tempScript = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".ps1");
Logger.Trace($"Creating temp script at path {tempScript}");
File.WriteAllText(tempScript, command);
RunScript(tempScript, asAdmin);
File.Delete(tempScript);
}
public void RunScript(string path, bool asAdmin = false, string arguments = null)
{
Logger.Trace($"Executing script at path {path} | Admin: {asAdmin} | Arguments: {arguments}");
var wow64Value = IntPtr.Zero;
// Disable Wow64 redirection so we can hit areas of the registry absolutely
Wow64DisableWow64FsRedirection(ref wow64Value);
var process = new Process();
process.StartInfo.FileName = "powershell.exe";
process.StartInfo.Arguments = $@"-ExecutionPolicy Unrestricted -File ""{path}""";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = false;
if (arguments != null)
process.StartInfo.Arguments += " " + arguments;
if (asAdmin)
{
process.StartInfo.Verb = "runas";
process.StartInfo.UseShellExecute = true;
}
process.Start();
process.WaitForExit();
Wow64RevertWow64FsRedirection(ref wow64Value);
}
public void RunScript(Game game, ScriptType type, string arguments = null)
{
var path = GetScriptFilePath(game, type);
if (File.Exists(path))
{
var contents = File.ReadAllText(path);
if (contents.StartsWith("# Requires Admin"))
RunScript(path, true, arguments);
else
RunScript(path, false, arguments);
}
}
public void RunScriptsAsAdmin(IEnumerable<string> paths, string arguments = null)
{
// Concatenate scripts
var sb = new StringBuilder();
Logger.Trace("Concatenating scripts...");
foreach (var path in paths)
{
var contents = File.ReadAllText(path);
sb.AppendLine(contents);
Logger.Trace($"Added {path}!");
}
Logger.Trace("Done concatenating!");
if (sb.Length > 0)
{
var scriptPath = Path.GetTempFileName();
Logger.Trace($"Creating temp script at path {scriptPath}");
File.WriteAllText(scriptPath, sb.ToString());
RunScript(scriptPath, true, arguments);
}
}
public void RunScripts(IEnumerable<Game> games, ScriptType type, string arguments = null)
{
List<string> scripts = new List<string>();
List<string> adminScripts = new List<string>();
foreach (var game in games)
{
var path = GetScriptFilePath(game, type);
if (!File.Exists(path))
continue;
var contents = File.ReadAllText(path);
if (contents.StartsWith("# Requires Admin"))
adminScripts.Add(path);
else
scripts.Add(path);
}
RunScriptsAsAdmin(adminScripts, arguments);
foreach (var script in scripts)
{
RunScript(script, false, arguments);
}
}
public static string GetScriptFilePath(Game game, ScriptType type)
{
Dictionary<ScriptType, string> filenames = new Dictionary<ScriptType, string>() {
{ ScriptType.Install, "_install.ps1" },
{ ScriptType.Uninstall, "_uninstall.ps1" },
{ ScriptType.NameChange, "_changename.ps1" },
{ ScriptType.KeyChange, "_changekey.ps1" }
};
var filename = filenames[type];
return Path.Combine(game.InstallDirectory, filename);
}
}
}

View File

@ -0,0 +1,61 @@
using LANCommander.SDK;
using Playnite.SDK;
using Playnite.SDK.Models;
using Playnite.SDK.Plugins;
namespace LANCommander.PlaynitePlugin
{
public class LANCommanderSaveController : ControllerBase
{
private static readonly ILogger Logger;
private LANCommanderLibraryPlugin Plugin;
public LANCommanderSaveController(LANCommanderLibraryPlugin plugin, Game game) : base(game)
{
Name = "Download save using LANCommander";
Plugin = plugin;
}
public void Download(Game game)
{
if (game != null)
{
Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress =>
{
progress.ProgressMaxValue = 100;
progress.CurrentProgressValue = 0;
var saveManager = new GameSaveManager(Plugin.LANCommanderClient);
saveManager.OnDownloadProgress += (downloadProgress) =>
{
progress.CurrentProgressValue = downloadProgress.ProgressPercentage;
};
saveManager.OnDownloadComplete += (downloadComplete) =>
{
progress.CurrentProgressValue = 100;
};
saveManager.Download(game.InstallDirectory);
// Lock the thread until the download is done
while (progress.CurrentProgressValue != 100) { }
},
new GlobalProgressOptions("Downloading latest save...")
{
IsIndeterminate = false,
Cancelable = false
});
}
}
public void Upload(Game game)
{
var saveManager = new GameSaveManager(Plugin.LANCommanderClient);
saveManager.Upload(game.InstallDirectory);
}
}
}

View File

@ -1,289 +0,0 @@
using LANCommander.SDK;
using Playnite.SDK;
using Playnite.SDK.Models;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Readers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace LANCommander.PlaynitePlugin.Services
{
internal class GameSaveService
{
private readonly LANCommanderClient LANCommander;
private readonly IPlayniteAPI PlayniteApi;
private readonly PowerShellRuntime PowerShellRuntime;
internal GameSaveService(LANCommanderClient lanCommander, IPlayniteAPI playniteApi, PowerShellRuntime powerShellRuntime)
{
LANCommander = lanCommander;
PlayniteApi = playniteApi;
PowerShellRuntime = powerShellRuntime;
}
internal void DownloadSave(Game game)
{
string tempFile = String.Empty;
if (game != null)
{
PlayniteApi.Dialogs.ActivateGlobalProgress(progress =>
{
progress.ProgressMaxValue = 100;
progress.CurrentProgressValue = 0;
var destination = LANCommander.DownloadLatestSave(Guid.Parse(game.GameId), (changed) =>
{
progress.CurrentProgressValue = changed.ProgressPercentage;
}, (complete) =>
{
progress.CurrentProgressValue = 100;
});
// Lock the thread until download is done
while (progress.CurrentProgressValue != 100)
{
}
tempFile = destination;
},
new GlobalProgressOptions("Downloading latest save...")
{
IsIndeterminate = false,
Cancelable = false
});
// Go into the archive and extract the files to the correct locations
try
{
var tempLocation = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempLocation);
ExtractFilesFromZip(tempFile, tempLocation);
var deserializer = new DeserializerBuilder()
.WithNamingConvention(new PascalCaseNamingConvention())
.Build();
var manifestContents = File.ReadAllText(Path.Combine(tempLocation, "_manifest.yml"));
var manifest = deserializer.Deserialize<GameManifest>(manifestContents);
#region Move files
foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "File"))
{
bool inInstallDir = savePath.Path.StartsWith("{InstallDir}");
string tempSavePath = Path.Combine(tempLocation, savePath.Id.ToString());
var tempSavePathFile = Path.Combine(tempSavePath, savePath.Path.Replace('/', '\\').Replace("{InstallDir}\\", ""));
var destination = Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', '\\').Replace("{InstallDir}", game.InstallDirectory));
if (File.Exists(tempSavePathFile))
{
// Is file, move file
if (File.Exists(destination))
File.Delete(destination);
File.Move(tempSavePathFile, destination);
}
else if (Directory.Exists(tempSavePath))
{
var files = Directory.GetFiles(tempSavePath, "*", SearchOption.AllDirectories);
if (inInstallDir)
{
foreach (var file in files)
{
if (inInstallDir)
{
// Files are in the game's install directory. Move them there from the save path.
destination = file.Replace(tempSavePath, savePath.Path.Replace('/', '\\').TrimEnd('\\').Replace("{InstallDir}", game.InstallDirectory));
if (File.Exists(destination))
File.Delete(destination);
File.Move(file, destination);
}
else
{
// Specified path is probably an absolute path, maybe with environment variables.
destination = Environment.ExpandEnvironmentVariables(file.Replace(tempSavePathFile, savePath.Path.Replace('/', '\\')));
if (File.Exists(destination))
File.Delete(destination);
File.Move(file, destination);
}
}
}
else
{
}
}
}
#endregion
#region Handle registry importing
var registryImportFilePath = Path.Combine(tempLocation, "_registry.reg");
if (File.Exists(registryImportFilePath))
{
var registryImportFileContents = File.ReadAllText(registryImportFilePath);
PowerShellRuntime.RunCommand($"regedit.exe /s \"{registryImportFilePath}\"", registryImportFileContents.Contains("HKEY_LOCAL_MACHINE"));
}
#endregion
// Clean up temp files
Directory.Delete(tempLocation, true);
}
catch (Exception ex)
{
}
}
}
internal void UploadSave(Game game)
{
var manifestPath = Path.Combine(game.InstallDirectory, "_manifest.yml");
if (File.Exists(manifestPath))
{
var deserializer = new DeserializerBuilder()
.WithNamingConvention(new PascalCaseNamingConvention())
.Build();
var manifest = deserializer.Deserialize<GameManifest>(File.ReadAllText(manifestPath));
var temp = Path.GetTempFileName();
if (manifest.SavePaths != null && manifest.SavePaths.Count() > 0)
{
using (var archive = ZipArchive.Create())
{
archive.DeflateCompressionLevel = SharpCompress.Compressors.Deflate.CompressionLevel.BestCompression;
#region Add files from defined paths
foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "File"))
{
var localPath = Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', '\\').Replace("{InstallDir}", game.InstallDirectory));
if (Directory.Exists(localPath))
{
AddDirectoryToZip(archive, localPath, localPath, savePath.Id);
}
else if (File.Exists(localPath))
{
archive.AddEntry(Path.Combine(savePath.Id.ToString(), savePath.Path.Replace("{InstallDir}/", "")), localPath);
}
}
#endregion
#region Add files from defined paths
foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "File"))
{
var localPath = Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', '\\').Replace("{InstallDir}", game.InstallDirectory));
if (Directory.Exists(localPath))
{
AddDirectoryToZip(archive, localPath, localPath, savePath.Id);
}
else if (File.Exists(localPath))
{
archive.AddEntry(Path.Combine(savePath.Id.ToString(), savePath.Path.Replace("{InstallDir}/", "")), localPath);
}
}
#endregion
#region Export registry keys
if (manifest.SavePaths.Any(sp => sp.Type == "Registry"))
{
List<string> tempRegFiles = new List<string>();
var exportCommand = new StringBuilder();
foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "Registry"))
{
var tempRegFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".reg");
exportCommand.AppendLine($"reg.exe export \"{savePath.Path.Replace(":\\", "\\")}\" \"{tempRegFile}\"");
tempRegFiles.Add(tempRegFile);
}
PowerShellRuntime.RunCommand(exportCommand.ToString());
var exportFile = new StringBuilder();
foreach (var tempRegFile in tempRegFiles)
{
exportFile.AppendLine(File.ReadAllText(tempRegFile));
File.Delete(tempRegFile);
}
archive.AddEntry("_registry.reg", new MemoryStream(Encoding.UTF8.GetBytes(exportFile.ToString())), true);
}
#endregion
archive.AddEntry("_manifest.yml", manifestPath);
using (var ms = new MemoryStream())
{
archive.SaveTo(ms);
ms.Seek(0, SeekOrigin.Begin);
var save = LANCommander.UploadSave(game.GameId, ms.ToArray());
}
}
}
}
}
private void AddDirectoryToZip(ZipArchive zipArchive, string path, string workingDirectory, Guid pathId)
{
foreach (var file in Directory.GetFiles(path))
{
// Oh man is this a hack. We should be removing only the working directory from the start,
// but we're making the assumption that the working dir put in actually prefixes the path.
// Also wtf, that Path.Combine is stripping the pathId out?
zipArchive.AddEntry(Path.Combine(pathId.ToString(), path.Substring(workingDirectory.Length), Path.GetFileName(file)), file);
}
foreach (var child in Directory.GetDirectories(path))
{
// See above
//ZipEntry entry = new ZipEntry(Path.Combine(pathId.ToString(), path.Substring(workingDirectory.Length), Path.GetFileName(path)));
//zipStream.PutNextEntry(entry);
//zipStream.CloseEntry();
AddDirectoryToZip(zipArchive, child, workingDirectory, pathId);
}
}
private void ExtractFilesFromZip(string zipPath, string destination)
{
using (var fs = File.OpenRead(zipPath))
using (var ts = new TrackableStream(fs))
using (var reader = ReaderFactory.Open(ts))
{
reader.WriteAllToDirectory(destination, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true
});
}
}
}
}

View File

@ -1,4 +1,6 @@
using LANCommander.SDK.Enums;
using LANCommander.SDK.Helpers;
using LANCommander.SDK.PowerShell;
using Playnite.SDK;
using Playnite.SDK.Models;
using Playnite.SDK.Plugins;
@ -12,29 +14,49 @@ namespace LANCommander.PlaynitePlugin
public static readonly ILogger Logger = LogManager.GetLogger();
private LANCommanderLibraryPlugin Plugin;
private PowerShellRuntime PowerShellRuntime;
public LANCommanderUninstallController(LANCommanderLibraryPlugin plugin, Game game) : base(game)
{
Name = "Uninstall LANCommander Game";
Plugin = plugin;
PowerShellRuntime = new PowerShellRuntime();
}
public override void Uninstall(UninstallActionArgs args)
{
try
{
PowerShellRuntime.RunScript(Game, ScriptType.Uninstall);
var gameManager = new LANCommander.SDK.GameManager(Plugin.LANCommanderClient, Plugin.Settings.InstallDirectory);
try
{
var scriptPath = ScriptHelper.GetScriptFilePath(Game.InstallDirectory, SDK.Enums.ScriptType.Uninstall);
if (!String.IsNullOrEmpty(scriptPath) && File.Exists(scriptPath))
{
var manifest = ManifestHelper.Read(Game.InstallDirectory);
var script = new PowerShellScript();
script.AddVariable("InstallDirectory", Game.InstallDirectory);
script.AddVariable("GameManifest", manifest);
script.AddVariable("DefaultInstallDirectory", Plugin.Settings.InstallDirectory);
script.AddVariable("ServerAddress", Plugin.Settings.ServerAddress);
script.UseFile(scriptPath);
script.Execute();
}
}
catch (Exception ex)
{
Logger.Error(ex, "There was an error running the uninstall script");
}
gameManager.Uninstall(Game.InstallDirectory);
}
catch (Exception ex)
{
Logger.Error(ex, "There was an error uninstalling the game");
}
catch { }
Logger.Trace("Attempting to delete install directory...");
if (!String.IsNullOrWhiteSpace(Game.InstallDirectory) && Directory.Exists(Game.InstallDirectory))
Directory.Delete(Game.InstallDirectory, true);
Logger.Trace("Deleted!");
InvokeOnUninstalled(new GameUninstalledEventArgs());
}

View File

@ -40,7 +40,10 @@ namespace LANCommander.PlaynitePlugin.Views
{
var beacon = beacons.First();
Context.ServerAddress = $"http://{beacon.Address.Address}:{beacon.Address.Port}";
if (!String.IsNullOrWhiteSpace(beacon.Data) && Uri.TryCreate(beacon.Data, UriKind.Absolute, out var beaconUri))
Context.ServerAddress = beaconUri.ToString();
else
Context.ServerAddress = $"http://{beacon.Address.Address}:{beacon.Address.Port}";
this.ServerAddress.Text = Context.ServerAddress;
@ -97,23 +100,20 @@ namespace LANCommander.PlaynitePlugin.Views
LoginButton.Content = "Logging in...";
}));
if (Plugin.LANCommander == null || Plugin.LANCommander.Client == null)
Plugin.LANCommander = new LANCommanderClient(Context.ServerAddress);
if (Plugin.LANCommanderClient == null)
Plugin.LANCommanderClient = new LANCommander.SDK.Client(Context.ServerAddress);
else
Plugin.LANCommander.Client.BaseUrl = new Uri(Context.ServerAddress);
Plugin.LANCommanderClient.UseServerAddress(Context.ServerAddress);
var response = await Plugin.LANCommander.AuthenticateAsync(Context.UserName, Context.Password);
var response = await Plugin.LANCommanderClient.AuthenticateAsync(Context.UserName, Context.Password);
Plugin.Settings.ServerAddress = Context.ServerAddress;
Plugin.Settings.AccessToken = response.AccessToken;
Plugin.Settings.RefreshToken = response.RefreshToken;
Plugin.Settings.PlayerName = Context.UserName;
Plugin.LANCommander.Token = new AuthToken()
{
AccessToken = response.AccessToken,
RefreshToken = response.RefreshToken,
};
var profile = Plugin.LANCommanderClient.GetProfile();
Plugin.Settings.PlayerName = String.IsNullOrWhiteSpace(profile.Alias) ? profile.UserName : profile.Alias;
// Probably unneeded, but why not be more secure?
Context.Password = String.Empty;
@ -124,6 +124,8 @@ namespace LANCommander.PlaynitePlugin.Views
}
catch (Exception ex)
{
Logger.Error(ex, ex.Message);
Plugin.PlayniteApi.Dialogs.ShowErrorMessage(ex.Message);
LoginButton.Dispatcher.Invoke(new System.Action(() =>
@ -142,24 +144,16 @@ namespace LANCommander.PlaynitePlugin.Views
RegisterButton.IsEnabled = false;
RegisterButton.Content = "Working...";
if (Plugin.LANCommander == null || Plugin.LANCommander.Client == null)
Plugin.LANCommander = new LANCommanderClient(Context.ServerAddress);
else
Plugin.LANCommander.Client.BaseUrl = new Uri(Context.ServerAddress);
if (Plugin.LANCommanderClient == null)
Plugin.LANCommanderClient = new LANCommander.SDK.Client(Context.ServerAddress);
var response = await Plugin.LANCommander.RegisterAsync(Context.UserName, Context.Password);
var response = await Plugin.LANCommanderClient.RegisterAsync(Context.UserName, Context.Password);
Plugin.Settings.ServerAddress = Context.ServerAddress;
Plugin.Settings.AccessToken = response.AccessToken;
Plugin.Settings.RefreshToken = response.RefreshToken;
Plugin.Settings.PlayerName = Context.UserName;
Plugin.LANCommander.Token = new AuthToken()
{
AccessToken = response.AccessToken,
RefreshToken = response.RefreshToken,
};
Context.Password = String.Empty;
Plugin.SavePluginSettings(Plugin.Settings);

View File

@ -14,30 +14,45 @@
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="100" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="75" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="Install Directory" />
<TextBox Name="PART_InstallDirectory" Grid.Column="2" Text="{Binding InstallDirectory}" />
<Button Grid.Column="4" Content="Browse" Click="SelectInstallDirectory_Click" VerticalAlignment="Center" Grid.ColumnSpan="2" />
</Grid>
<Grid Grid.Row="1" Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="75" />
</Grid.ColumnDefinitions>
<Grid Height="40" Grid.Row="1">
<Label Grid.Column="0" Content="Server Address" />
<TextBox Name="PART_ServerAddress" Grid.Column="2" Text="{Binding ServerAddress}" IsEnabled="false" />
</Grid>
<Grid Grid.Row="2" Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Name="PART_AuthenticationButton" Content="Authenticate" Grid.Column="0" Click="AuthenticateButton_Click" VerticalAlignment="Center" />
<Label Name="PART_AuthenticateLabel" Margin="20,0,0,0" VerticalAlignment="Center" Grid.Column="1" />
<Button Name="PART_DisconnectButton" Content="Disconnect" Grid.Column="0" Click="DisconnectButton_Click" VerticalAlignment="Center" />
<Label Name="PART_AuthenticateLabel" Margin="20,0,0,0" VerticalAlignment="Center" Grid.Column="3" />
</Grid>
</Grid>
</UserControl>

View File

@ -39,6 +39,7 @@ namespace LANCommander.PlaynitePlugin
PART_AuthenticateLabel.Content = "Checking authentication status...";
PART_AuthenticationButton.IsEnabled = false;
PART_InstallDirectory.Text = Settings.InstallDirectory;
PART_ServerAddress.Text = Settings.ServerAddress;
var token = new AuthToken()
{
@ -46,7 +47,7 @@ namespace LANCommander.PlaynitePlugin
RefreshToken = Settings.RefreshToken,
};
var task = Task.Run(() => Plugin.LANCommander.ValidateToken(token))
var task = Task.Run(() => Plugin.LANCommanderClient.ValidateToken(token))
.ContinueWith(antecedent =>
{
try
@ -57,11 +58,17 @@ namespace LANCommander.PlaynitePlugin
{
PART_AuthenticateLabel.Content = "Authentication failed!";
PART_AuthenticationButton.IsEnabled = true;
PART_AuthenticationButton.Visibility = Visibility.Visible;
PART_DisconnectButton.IsEnabled = false;
PART_DisconnectButton.Visibility = Visibility.Hidden;
}
else
{
PART_AuthenticateLabel.Content = "Connection established!";
PART_AuthenticationButton.IsEnabled = false;
PART_AuthenticationButton.Visibility = Visibility.Hidden;
PART_DisconnectButton.IsEnabled = true;
PART_DisconnectButton.Visibility = Visibility.Visible;
}
}));
}
@ -74,9 +81,22 @@ namespace LANCommander.PlaynitePlugin
private void AuthenticateButton_Click(object sender, RoutedEventArgs e)
{
var authWindow = Plugin.ShowAuthenticationWindow();
var authWindow = Plugin.ShowAuthenticationWindow(Settings.ServerAddress, AuthWindow_Closed);
}
authWindow.Closed += AuthWindow_Closed;
private void DisconnectButton_Click(object sender, RoutedEventArgs e)
{
Plugin.Settings.AccessToken = String.Empty;
Plugin.Settings.RefreshToken = String.Empty;
Plugin.LANCommanderClient.UseToken(null);
Plugin.SavePluginSettings(Plugin.Settings);
PART_AuthenticateLabel.Content = "Not Authenticated";
PART_AuthenticationButton.IsEnabled = true;
PART_AuthenticationButton.Visibility = Visibility.Visible;
PART_DisconnectButton.IsEnabled = false;
PART_DisconnectButton.Visibility = Visibility.Hidden;
}
private void AuthWindow_Closed(object sender, EventArgs e)

View File

@ -8,7 +8,7 @@
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Text.Json" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.0.0.1" newVersion="5.0.0.1" />
<bindingRedirect oldVersion="0.0.0.0-7.0.0.3" newVersion="7.0.0.3" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Threading.Tasks.Extensions" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
@ -22,6 +22,14 @@
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.2" newVersion="4.0.1.2" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Bcl.AsyncInterfaces" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Text.Encodings.Web" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@ -0,0 +1,50 @@
AddonId: LANCommander.PlaynitePlugin_48e1bac7-e0a0-45d7-ba83-36f5e9e959fc
Packages:
- Version: 0.1.2
RequiredApiVersion: 6.0.0
ReleaseDate: 2023-09-15
PackageUrl: https://github.com/LANCommander/LANCommander/releases/download/v0.1.2/LANCommander.PlaynitePlugin_48e1bac7-e0a0-45d7-ba83-36f5e9e959fc-v0.1.2.pext
Changelog:
- Initial submission to Playnite addon database
- Version: 0.1.3
RequiredApiVersion: 6.0.0
ReleaseDate: 2023-09-21
PackageUrl: https://github.com/LANCommander/LANCommander/releases/download/v0.1.3/LANCommander.PlaynitePlugin_48e1bac7-e0a0-45d7-ba83-36f5e9e959fc_v0.1.3.pext
Changelog:
- Added the ability to cancel installs
- Version: 0.2.0
RequiredApiVersion: 6.0.0
ReleaseDate: 2023-10-28
PackageUrl: https://github.com/LANCommander/LANCommander/releases/download/v0.2.0/LANCommander.PlaynitePlugin_48e1bac7-e0a0-45d7-ba83-36f5e9e959fc_0_2_0.pext
Changelog:
- Player name is now persisted across machines and is stored as the user's profile name
- Added support for servers to bundle redistributables with games
- Added support for opening authentication window using Playnite API
- Version: 0.2.1
RequiredApiVersion: 6.0.0
ReleaseDate: 2023-11-03
PackageUrl: https://github.com/LANCommander/LANCommander/releases/download/v0.2.1/LANCommander.PlaynitePlugin_48e1bac7-e0a0-45d7-ba83-36f5e9e959fc_0_2_1.pext
Changelog:
- LANCommander servers can now provide game art to Playnite clients
- Added server address to addon settings
- Added disconnect button to addon settings
- Version: 0.2.2
RequiredApiVersion: 6.0.0
ReleaseDate: 2023-11-20
PackageUrl: https://github.com/LANCommander/LANCommander/releases/download/v0.2.2/LANCommander.PlaynitePlugin_48e1bac7-e0a0-45d7-ba83-36f5e9e959fc_0_2_2.pext
Changelog:
- _manifest.yml files in the game's install directory now includes the game's ID
- Installation progress dialog now shows the download percentage and transfer speed
- Full game download will be skipped if the game files already exist, see full release notes for more details
- Play sessions are now recorded to the server with user ID, start time, and end time
- Connection status now updates correctly when authenticating through addon settings
- Version: 0.3.0
RequiredApiVersion: 6.0.0
ReleaseDate: 2023-12-03
PackageUrl: https://github.com/LANCommander/LANCommander/releases/download/v0.3.0/LANCommander.PlaynitePlugin_48e1bac7-e0a0-45d7-ba83-36f5e9e959fc_0_3_0.pext
Changelog:
- Save paths now support regex patterns (experimental)
- Fixed redistributable archive downloading
- Fixed redistributable scripts not being able to run as admin if marked as such
- Fixed game save downloading
- Fixed addon loading of YamlDotNet library

View File

@ -1,20 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Bcl.AsyncInterfaces" version="5.0.0" targetFramework="net462" />
<package id="NuGet.CommandLine" version="4.4.3" targetFramework="net462" developmentDependency="true" />
<package id="PlayniteSDK" version="6.9.0" targetFramework="net462" />
<package id="ByteSize" version="2.1.1" targetFramework="net462" />
<package id="Microsoft.Bcl.AsyncInterfaces" version="8.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.DependencyInjection.Abstractions" version="8.0.0" targetFramework="net462" />
<package id="Microsoft.Extensions.Logging.Abstractions" version="8.0.0" targetFramework="net462" />
<package id="NuGet.CommandLine" version="6.8.0" targetFramework="net462" developmentDependency="true" />
<package id="PlayniteSDK" version="6.10.0" targetFramework="net462" />
<package id="PowerShellStandard.Library" version="5.1.1" targetFramework="net462" />
<package id="RestSharp" version="106.15.0" targetFramework="net462" />
<package id="rix0rrr.BeaconLib" version="1.0.2" targetFramework="net462" />
<package id="SharpCompress" version="0.33.0" targetFramework="net462" />
<package id="SharpCompress" version="0.34.2" targetFramework="net462" />
<package id="System.Buffers" version="4.5.1" targetFramework="net462" />
<package id="System.Memory" version="4.5.5" targetFramework="net462" />
<package id="System.Numerics.Vectors" version="4.5.0" targetFramework="net462" />
<package id="System.Runtime.CompilerServices.Unsafe" version="6.0.0" targetFramework="net462" />
<package id="System.Text.Encoding.CodePages" version="7.0.0" targetFramework="net462" />
<package id="System.Text.Encodings.Web" version="5.0.0" targetFramework="net462" />
<package id="System.Text.Json" version="5.0.1" targetFramework="net462" />
<package id="System.Text.Encoding.CodePages" version="8.0.0" targetFramework="net462" />
<package id="System.Text.Encodings.Web" version="8.0.0" targetFramework="net462" />
<package id="System.Text.Json" version="8.0.0" targetFramework="net462" />
<package id="System.Threading.Tasks.Extensions" version="4.5.4" targetFramework="net462" />
<package id="System.ValueTuple" version="4.5.0" targetFramework="net462" />
<package id="YamlDotNet" version="5.4.0" targetFramework="net462" />
<package id="ZstdSharp.Port" version="0.7.2" targetFramework="net462" />
</packages>

View File

@ -0,0 +1,64 @@
using LANCommander.PowerShell.Cmdlets;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Linq;
namespace LANCommander.PowerShell.Tests
{
[TestClass]
public class CmdletTests
{
[TestMethod]
public void ConvertToSerializedBase64ShouldBeDeserializable()
{
var testPhrase = "Hello world! This should be deserializable back to its original form.";
var encodingCmdlet = new ConvertToSerializedBase64Cmdlet()
{
Input = testPhrase
};
var encodingResults = encodingCmdlet.Invoke().OfType<string>().ToList();
Assert.AreEqual(1, encodingResults.Count);
var decodingCmdlet = new ConvertFromSerializedBase64Cmdlet()
{
Input = encodingResults.First()
};
var decodingResults = decodingCmdlet.Invoke().OfType<string>().ToList();
Assert.AreEqual(1, encodingResults.Count);
Assert.AreEqual(testPhrase, decodingResults.First());
}
[TestMethod]
[DataRow(640, 480, 640, 360, 16, 9)]
[DataRow(1024, 768, 1024, 576, 16, 9)]
[DataRow(1600, 1200, 1600, 900, 16, 9)]
[DataRow(1920, 1080, 1440, 1080, 4, 3)]
[DataRow(1366, 1024, 1024, 768, 4, 3)]
[DataRow(854, 480, 640, 480, 4, 3)]
public void ConvertAspectRatioShouldReturnCorrectBounds(int x1, int y1, int x2, int y2, int ratioX, int ratioY)
{
var aspectRatio = (double)ratioX / (double)ratioY;
var cmdlet = new ConvertAspectRatioCmdlet()
{
AspectRatio = aspectRatio,
Width = x1,
Height = y1
};
var output = cmdlet.Invoke().OfType<DisplayResolution>().ToList();
Assert.AreEqual(1, output.Count);
var bounds = output.First();
Assert.AreEqual(x2, bounds.Width);
Assert.AreEqual(y2, bounds.Height);
}
}
}

View File

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\MSTest.TestAdapter.2.2.10\build\net46\MSTest.TestAdapter.props" Condition="Exists('..\packages\MSTest.TestAdapter.2.2.10\build\net46\MSTest.TestAdapter.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{D7069A13-F0AA-4CBF-9013-4276F130A6DD}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>LANCommander.PowerShell.Tests</RootNamespace>
<AssemblyName>LANCommander.PowerShell.Tests</AssemblyName>
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">15.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>
<IsCodedUITest>False</IsCodedUITest>
<TestProjectType>UnitTest</TestProjectType>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Bcl.AsyncInterfaces, Version=5.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Bcl.AsyncInterfaces.5.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll</HintPath>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\MSTest.TestFramework.2.2.10\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll</HintPath>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\MSTest.TestFramework.2.2.10\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll</HintPath>
</Reference>
<Reference Include="RestSharp, Version=106.15.0.0, Culture=neutral, PublicKeyToken=598062e77f915f75, processorArchitecture=MSIL">
<HintPath>..\packages\RestSharp.106.15.0\lib\net452\RestSharp.dll</HintPath>
</Reference>
<Reference Include="SharpCompress, Version=0.34.2.0, Culture=neutral, PublicKeyToken=afb0a02973931d96, processorArchitecture=MSIL">
<HintPath>..\packages\SharpCompress.0.34.2\lib\net462\SharpCompress.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll</HintPath>
</Reference>
<Reference Include="System.Core" />
<Reference Include="System.Management.Automation, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<Reference Include="System.Memory, Version=4.0.1.2, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll</HintPath>
</Reference>
<Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors, Version=4.1.4.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
</Reference>
<Reference Include="System.Text.Encoding.CodePages, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Text.Encoding.CodePages.8.0.0\lib\net462\System.Text.Encoding.CodePages.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.Web" />
<Reference Include="YamlDotNet, Version=5.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\YamlDotNet.5.4.0\lib\net45\YamlDotNet.dll</HintPath>
</Reference>
<Reference Include="ZstdSharp, Version=0.7.4.0, Culture=neutral, PublicKeyToken=8d151af33a4ad5cf, processorArchitecture=MSIL">
<HintPath>..\packages\ZstdSharp.Port.0.7.4\lib\net462\ZstdSharp.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Cmdlets.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LANCommander.PowerShell\LANCommander.PowerShell.csproj">
<Project>{807943bf-0c7d-4ed3-8393-cfee64e3138c}</Project>
<Name>LANCommander.PowerShell</Name>
</ProjectReference>
<ProjectReference Include="..\LANCommander.SDK\LANCommander.SDK.csproj">
<Project>{4c2a71fd-a30b-4d62-888a-4ef843d8e506}</Project>
<Name>LANCommander.SDK</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\MSTest.TestAdapter.2.2.10\build\net46\MSTest.TestAdapter.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSTest.TestAdapter.2.2.10\build\net46\MSTest.TestAdapter.props'))" />
<Error Condition="!Exists('..\packages\MSTest.TestAdapter.2.2.10\build\net46\MSTest.TestAdapter.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSTest.TestAdapter.2.2.10\build\net46\MSTest.TestAdapter.targets'))" />
</Target>
<Import Project="..\packages\MSTest.TestAdapter.2.2.10\build\net46\MSTest.TestAdapter.targets" Condition="Exists('..\packages\MSTest.TestAdapter.2.2.10\build\net46\MSTest.TestAdapter.targets')" />
</Project>

View File

@ -0,0 +1,20 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("LANCommander.PowerShell.Tests")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("LANCommander.PowerShell.Tests")]
[assembly: AssemblyCopyright("Copyright © 2023")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("d7069a13-f0aa-4cbf-9013-4276f130a6dd")]
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Bcl.AsyncInterfaces" version="5.0.0" targetFramework="net462" />
<package id="MSTest.TestAdapter" version="2.2.10" targetFramework="net462" />
<package id="MSTest.TestFramework" version="2.2.10" targetFramework="net462" />
<package id="RestSharp" version="106.15.0" targetFramework="net462" />
<package id="SharpCompress" version="0.34.2" targetFramework="net462" />
<package id="System.Buffers" version="4.5.1" targetFramework="net462" />
<package id="System.Memory" version="4.5.5" targetFramework="net462" />
<package id="System.Numerics.Vectors" version="4.5.0" targetFramework="net462" />
<package id="System.Runtime.CompilerServices.Unsafe" version="6.0.0" targetFramework="net462" />
<package id="System.Text.Encoding.CodePages" version="8.0.0" targetFramework="net462" />
<package id="System.Threading.Tasks.Extensions" version="4.5.4" targetFramework="net462" />
<package id="YamlDotNet" version="5.4.0" targetFramework="net462" />
<package id="ZstdSharp.Port" version="0.7.4" targetFramework="net462" />
</packages>

View File

@ -0,0 +1,45 @@
using System;
using System.Management.Automation;
namespace LANCommander.PowerShell.Cmdlets
{
public class DisplayResolution
{
public int Width { get; set; }
public int Height { get; set; }
}
[Cmdlet(VerbsData.Convert, "AspectRatio")]
[OutputType(typeof(string))]
public class ConvertAspectRatioCmdlet : Cmdlet
{
[Parameter(Mandatory = true, Position = 0)]
public int Width { get; set; }
[Parameter(Mandatory = true, Position = 1)]
public int Height { get; set; }
[Parameter(Mandatory = true, Position = 2)]
public double AspectRatio { get; set; }
protected override void ProcessRecord()
{
var resolution = new DisplayResolution();
// Display is wider, pillar box
if ((Width / Height) < AspectRatio)
{
resolution.Width = (int)Math.Ceiling(Height * AspectRatio);
resolution.Height = Height;
}
// Letterbox
else
{
resolution.Width = Width;
resolution.Height = (int)Math.Ceiling(Width * (1 / AspectRatio));
}
WriteObject(resolution);
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Text;
using System.Threading.Tasks;
namespace LANCommander.PowerShell.Cmdlets
{
[Cmdlet(VerbsData.ConvertFrom, "SerializedBase64")]
[OutputType(typeof(object))]
public class ConvertFromSerializedBase64Cmdlet : Cmdlet
{
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Input { get; set; }
protected override void ProcessRecord()
{
var xml = Encoding.UTF8.GetString(Convert.FromBase64String(Input));
WriteObject(PSSerializer.Deserialize(xml));
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Text;
using System.Threading.Tasks;
namespace LANCommander.PowerShell.Cmdlets
{
[Cmdlet(VerbsData.ConvertTo, "SerializedBase64")]
[OutputType(typeof(object))]
public class ConvertToSerializedBase64Cmdlet : Cmdlet
{
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public object Input { get; set; }
protected override void ProcessRecord()
{
var output = Convert.ToBase64String(Encoding.UTF8.GetBytes(PSSerializer.Serialize(Input)));
WriteObject(output);
}
}
}

View File

@ -0,0 +1,48 @@
using LANCommander.SDK;
using LANCommander.SDK.Helpers;
using System.Management.Automation;
namespace LANCommander.PowerShell.Cmdlets
{
[Cmdlet(VerbsData.ConvertTo, "StringBytes")]
[OutputType(typeof(byte[]))]
public class ConvertToStringBytesCmdlet : Cmdlet
{
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Input { get; set; }
[Parameter]
public bool Utf16 { get; set; } = false;
[Parameter]
public bool BigEndian { get; set; } = false;
[Parameter]
public int MaxLength { get; set; } = 0;
[Parameter]
public int MinLength { get; set; } = 0;
protected override void ProcessRecord()
{
byte[] output;
if (MaxLength > 0 && Input.Length > MaxLength)
Input = Input.Substring(0, MaxLength);
if (MinLength > 0 && MinLength < MaxLength)
Input = Input.PadRight(MinLength, '\0');
else if (MinLength > 0)
Input = Input.PadRight(MaxLength, '\0');
if (Utf16 && BigEndian)
output = System.Text.Encoding.BigEndianUnicode.GetBytes(Input);
else if (Utf16)
output = System.Text.Encoding.Unicode.GetBytes(Input);
else
output = System.Text.Encoding.ASCII.GetBytes(Input);
WriteObject(output);
}
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.IO;
using System.Management.Automation;
namespace LANCommander.PowerShell.Cmdlets
{
[Cmdlet(VerbsData.Edit, "PatchBinary")]
[OutputType(typeof(string))]
public class EditPatchBinaryCmdlet : Cmdlet
{
[Parameter(Mandatory = true, Position = 0)]
public long Offset { get; set; }
[Parameter(Mandatory = true, Position = 1)]
public byte[] Data { get; set; }
[Parameter(Mandatory = true, Position = 2)]
public string FilePath { get; set; }
protected override void ProcessRecord()
{
using (var writer = File.OpenWrite(FilePath))
{
writer.Seek(Offset, SeekOrigin.Begin);
writer.Write(Data, 0, Data.Length);
}
}
}
}

View File

@ -0,0 +1,19 @@
using LANCommander.SDK;
using LANCommander.SDK.Helpers;
using System.Management.Automation;
namespace LANCommander.PowerShell.Cmdlets
{
[Cmdlet(VerbsCommon.Get, "GameManifest")]
[OutputType(typeof(GameManifest))]
public class GetGameManifestCmdlet : Cmdlet
{
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Path { get; set; }
protected override void ProcessRecord()
{
WriteObject(ManifestHelper.Read(Path));
}
}
}

View File

@ -0,0 +1,18 @@
using System.Linq;
using System.Management.Automation;
using System.Windows.Forms;
namespace LANCommander.PowerShell.Cmdlets
{
[Cmdlet(VerbsCommon.Get, "PrimaryDisplay")]
[OutputType(typeof(string))]
public class GetPrimaryDisplayCmdlet : Cmdlet
{
protected override void ProcessRecord()
{
var screens = Screen.AllScreens;
WriteObject(screens.First(s => s.Primary));
}
}
}

View File

@ -0,0 +1,132 @@
using LANCommander.SDK;
using LANCommander.SDK.Helpers;
using LANCommander.SDK.PowerShell;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Windows.Forms;
namespace LANCommander.PowerShell.Cmdlets
{
[Cmdlet(VerbsLifecycle.Install, "Game")]
[OutputType(typeof(string))]
public class InstallGameCmdlet : Cmdlet
{
[Parameter(Mandatory = true)]
public Client Client { get; set; }
[Parameter(Mandatory = true)]
public Guid Id { get; set; }
[Parameter(Mandatory = false)]
public string InstallDirectory { get; set; } = "C:\\Games";
protected override void ProcessRecord()
{
var gameManager = new GameManager(Client, InstallDirectory);
var game = Client.GetGame(Id);
var progress = new ProgressRecord(1, $"Installing {game.Title}", "Progress:");
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
gameManager.OnArchiveExtractionProgress += (long position, long length) =>
{
// Only update a max of every 500ms
if (stopwatch.ElapsedMilliseconds > 500)
{
progress.PercentComplete = (int)Math.Ceiling((position / (decimal)length) * 100);
WriteProgress(progress);
stopwatch.Restart();
}
};
var installDirectory = gameManager.Install(Id);
stopwatch.Stop();
RunInstallScript(installDirectory);
RunNameChangeScript(installDirectory);
RunKeyChangeScript(installDirectory);
WriteObject(installDirectory);
}
private int RunInstallScript(string installDirectory)
{
var manifest = ManifestHelper.Read(installDirectory);
var path = ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.Install);
if (File.Exists(path))
{
var script = new PowerShellScript();
script.AddVariable("InstallDirectory", installDirectory);
script.AddVariable("GameManifest", manifest);
script.AddVariable("DefaultInstallDirectory", InstallDirectory);
script.AddVariable("ServerAddress", Client.BaseUrl);
script.UseFile(ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.Install));
return script.Execute();
}
return 0;
}
private int RunNameChangeScript(string installDirectory)
{
var user = Client.GetProfile();
var manifest = ManifestHelper.Read(installDirectory);
var path = ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.NameChange);
if (File.Exists(path))
{
var script = new PowerShellScript();
script.AddVariable("InstallDirectory", installDirectory);
script.AddVariable("GameManifest", manifest);
script.AddVariable("DefaultInstallDirectory", InstallDirectory);
script.AddVariable("ServerAddress", Client.BaseUrl);
script.AddVariable("OldPlayerAlias", "");
script.AddVariable("NewPlayerAlias", user.UserName);
script.UseFile(path);
return script.Execute();
}
return 0;
}
private int RunKeyChangeScript(string installDirectory)
{
var manifest = ManifestHelper.Read(installDirectory);
var path = ScriptHelper.GetScriptFilePath(installDirectory, SDK.Enums.ScriptType.KeyChange);
if (File.Exists(path))
{
var script = new PowerShellScript();
var key = Client.GetAllocatedKey(manifest.Id);
script.AddVariable("InstallDirectory", installDirectory);
script.AddVariable("GameManifest", manifest);
script.AddVariable("DefaultInstallDirectory", InstallDirectory);
script.AddVariable("ServerAddress", Client.BaseUrl);
script.AddVariable("AllocatedKey", key);
script.UseFile(path);
return script.Execute();
}
return 0;
}
}
}

View File

@ -0,0 +1,42 @@
using LANCommander.SDK;
using LANCommander.SDK.Helpers;
using LANCommander.SDK.PowerShell;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Windows.Forms;
namespace LANCommander.PowerShell.Cmdlets
{
[Cmdlet(VerbsLifecycle.Uninstall, "Game")]
[OutputType(typeof(string))]
public class UninstallGameCmdlet : Cmdlet
{
[Parameter(Mandatory = true)]
public string InstallDirectory { get; set; }
protected override void ProcessRecord()
{
var scriptPath = ScriptHelper.GetScriptFilePath(InstallDirectory, SDK.Enums.ScriptType.Uninstall);
if (!String.IsNullOrEmpty(scriptPath) && File.Exists(scriptPath))
{
var manifest = ManifestHelper.Read(InstallDirectory);
var script = new PowerShellScript();
script.AddVariable("InstallDirectory", InstallDirectory);
script.AddVariable("GameManifest", manifest);
script.UseFile(scriptPath);
script.Execute();
}
var gameManager = new GameManager(null, InstallDirectory);
gameManager.Uninstall(InstallDirectory);
}
}
}

View File

@ -0,0 +1,24 @@
using LANCommander.SDK;
using LANCommander.SDK.Helpers;
using System.Management.Automation;
namespace LANCommander.PowerShell.Cmdlets
{
[Cmdlet(VerbsCommunications.Write, "GameManifest")]
[OutputType(typeof(string))]
public class WriteGameManifestCmdlet : Cmdlet
{
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Path { get; set; }
[Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public GameManifest Manifest { get; set; }
protected override void ProcessRecord()
{
var destination = ManifestHelper.Write(Manifest, Path);
WriteObject(destination);
}
}
}

View File

@ -0,0 +1,36 @@
using System.IO;
using System.Management.Automation;
using System.Text.RegularExpressions;
namespace LANCommander.PowerShell.Cmdlets
{
[Cmdlet(VerbsCommunications.Write, "ReplaceContentInFile")]
[OutputType(typeof(string))]
public class ReplaceContentInFileCmdlet : Cmdlet
{
[Parameter(Mandatory = true, Position = 0)]
public string Pattern { get; set; }
[Parameter(Mandatory = true, Position = 1)]
public string Substitution { get; set; }
[Parameter(Mandatory = true, Position = 2)]
public string FilePath { get; set; }
protected override void ProcessRecord()
{
if (File.Exists(FilePath))
{
var contents = File.ReadAllText(FilePath);
var regex = new Regex(Pattern, RegexOptions.Multiline);
contents = regex.Replace(contents, Substitution);
File.WriteAllText(FilePath, contents);
WriteObject(contents);
}
}
}
}

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{807943BF-0C7D-4ED3-8393-CFEE64E3138C}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>LANCommander.PowerShell</RootNamespace>
<AssemblyName>LANCommander.PowerShell</AssemblyName>
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Management.Automation, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\PowerShellStandard.Library.5.1.1\lib\net452\System.Management.Automation.dll</HintPath>
</Reference>
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Cmdlets\ConvertTo-SerializedBase64.cs" />
<Compile Include="Cmdlets\ConvertFrom-SerializedBase64.cs" />
<Compile Include="Cmdlets\Edit-PatchBinary.cs" />
<Compile Include="Cmdlets\Uninstall-Game.cs" />
<Compile Include="Cmdlets\Install-Game.cs" />
<Compile Include="Cmdlets\Write-ReplaceContentInFile.cs" />
<Compile Include="Cmdlets\ConvertTo-StringBytes.cs" />
<Compile Include="Cmdlets\Get-PrimaryDisplay.cs" />
<Compile Include="Cmdlets\Convert-AspectRatio.cs" />
<Compile Include="Cmdlets\Get-GameManifest.cs" />
<Compile Include="Cmdlets\Write-GameManifest.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="LANCommander.PowerShell.psd1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LANCommander.SDK\LANCommander.SDK.csproj">
<Project>{4c2a71fd-a30b-4d62-888a-4ef843d8e506}</Project>
<Name>LANCommander.SDK</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

Binary file not shown.

View File

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("LANCommander.PowerShell")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("LANCommander.PowerShell")]
[assembly: AssemblyCopyright("Copyright © 2023")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 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("807943bf-0c7d-4ed3-8393-cfee64e3138c")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="PowerShellStandard.Library" version="5.1.1" targetFramework="net472" />
</packages>

View File

@ -1,6 +1,6 @@
using LANCommander.SDK;
using LANCommander.SDK.Models;
using Playnite.SDK;
using Microsoft.Extensions.Logging;
using RestSharp;
using System;
using System.Collections.Generic;
@ -11,19 +11,33 @@ using System.Net;
using System.Net.NetworkInformation;
using System.Threading.Tasks;
namespace LANCommander.PlaynitePlugin
namespace LANCommander.SDK
{
internal class LANCommanderClient
public class Client
{
public static readonly ILogger Logger = LogManager.GetLogger();
private readonly ILogger Logger;
public readonly RestClient Client;
public AuthToken Token;
private RestClient ApiClient;
private AuthToken Token;
public LANCommanderClient(string baseUrl)
public string BaseUrl;
public Client(string baseUrl)
{
if (!String.IsNullOrWhiteSpace(baseUrl))
Client = new RestClient(baseUrl);
BaseUrl = baseUrl;
if (!String.IsNullOrWhiteSpace(BaseUrl))
ApiClient = new RestClient(BaseUrl);
}
public Client(string baseUrl, ILogger logger)
{
BaseUrl = baseUrl;
if (!String.IsNullOrWhiteSpace(BaseUrl))
ApiClient = new RestClient(BaseUrl);
Logger = logger;
}
private T PostRequest<T>(string route, object body)
@ -32,7 +46,17 @@ namespace LANCommander.PlaynitePlugin
.AddJsonBody(body)
.AddHeader("Authorization", $"Bearer {Token.AccessToken}");
var response = Client.Post<T>(request);
var response = ApiClient.Post<T>(request);
return response.Data;
}
private T PostRequest<T>(string route)
{
var request = new RestRequest(route)
.AddHeader("Authorization", $"Bearer {Token.AccessToken}");
var response = ApiClient.Post<T>(request);
return response.Data;
}
@ -42,7 +66,7 @@ namespace LANCommander.PlaynitePlugin
var request = new RestRequest(route)
.AddHeader("Authorization", $"Bearer {Token.AccessToken}");
var response = Client.Get<T>(request);
var response = ApiClient.Get<T>(request);
return response.Data;
}
@ -58,7 +82,7 @@ namespace LANCommander.PlaynitePlugin
client.DownloadProgressChanged += (s, e) => progressHandler(e);
client.DownloadFileCompleted += (s, e) => completeHandler(e);
client.DownloadFileAsync(new Uri($"{Client.BaseUrl}{route}"), tempFile);
client.DownloadFileAsync(new Uri($"{ApiClient.BaseUrl}{route}"), tempFile);
return tempFile;
}
@ -72,14 +96,14 @@ namespace LANCommander.PlaynitePlugin
client.Headers.Add("Authorization", $"Bearer {Token.AccessToken}");
var ws = client.OpenRead(new Uri($"{Client.BaseUrl}{route}"));
var ws = client.OpenRead(new Uri($"{ApiClient.BaseUrl}{route}"));
return new TrackableStream(ws, true, Convert.ToInt64(client.ResponseHeaders["Content-Length"]));
}
public async Task<AuthResponse> AuthenticateAsync(string username, string password)
public async Task<AuthToken> AuthenticateAsync(string username, string password)
{
var response = await Client.ExecuteAsync<AuthResponse>(new RestRequest("/api/Auth", Method.POST).AddJsonBody(new AuthRequest()
var response = await ApiClient.ExecuteAsync<AuthResponse>(new RestRequest("/api/Auth", Method.POST).AddJsonBody(new AuthRequest()
{
UserName = username,
Password = password
@ -88,7 +112,14 @@ namespace LANCommander.PlaynitePlugin
switch (response.StatusCode)
{
case HttpStatusCode.OK:
return response.Data;
Token = new AuthToken
{
AccessToken = response.Data.AccessToken,
RefreshToken = response.Data.RefreshToken,
Expiration = response.Data.Expiration
};
return Token;
case HttpStatusCode.Forbidden:
case HttpStatusCode.BadRequest:
@ -100,9 +131,9 @@ namespace LANCommander.PlaynitePlugin
}
}
public async Task<AuthResponse> RegisterAsync(string username, string password)
public async Task<AuthToken> RegisterAsync(string username, string password)
{
var response = await Client.ExecuteAsync<AuthResponse>(new RestRequest("/api/auth/register", Method.POST).AddJsonBody(new AuthRequest()
var response = await ApiClient.ExecuteAsync<AuthResponse>(new RestRequest("/api/auth/register", Method.POST).AddJsonBody(new AuthRequest()
{
UserName = username,
Password = password
@ -111,7 +142,14 @@ namespace LANCommander.PlaynitePlugin
switch (response.StatusCode)
{
case HttpStatusCode.OK:
return response.Data;
Token = new AuthToken
{
AccessToken = response.Data.AccessToken,
RefreshToken = response.Data.RefreshToken,
Expiration = response.Data.Expiration
};
return Token;
case HttpStatusCode.BadRequest:
case HttpStatusCode.Forbidden:
@ -125,33 +163,45 @@ namespace LANCommander.PlaynitePlugin
public async Task<bool> PingAsync()
{
var response = await Client.ExecuteAsync(new RestRequest("/api/Ping", Method.GET));
var response = await ApiClient.ExecuteAsync(new RestRequest("/api/Ping", Method.GET));
return response.StatusCode == HttpStatusCode.OK;
}
public AuthResponse RefreshToken(AuthToken token)
public AuthToken RefreshToken(AuthToken token)
{
Logger.Trace("Refreshing token...");
Logger?.LogTrace("Refreshing token...");
var request = new RestRequest("/api/Auth/Refresh")
.AddJsonBody(token);
var response = Client.Post<AuthResponse>(request);
var response = ApiClient.Post<AuthResponse>(request);
if (response.StatusCode != HttpStatusCode.OK)
throw new WebException(response.ErrorMessage);
return response.Data;
Token = new AuthToken
{
AccessToken = response.Data.AccessToken,
RefreshToken = response.Data.RefreshToken,
Expiration = response.Data.Expiration
};
return Token;
}
public bool ValidateToken()
{
return ValidateToken(Token);
}
public bool ValidateToken(AuthToken token)
{
Logger.Trace("Validating token...");
Logger?.LogTrace("Validating token...");
if (token == null)
{
Logger.Trace("Token is null!");
Logger?.LogTrace("Token is null!");
return false;
}
@ -160,22 +210,33 @@ namespace LANCommander.PlaynitePlugin
if (String.IsNullOrEmpty(token.AccessToken) || String.IsNullOrEmpty(token.RefreshToken))
{
Logger.Trace("Token is empty!");
Logger?.LogTrace("Token is empty!");
return false;
}
var response = Client.Post(request);
var response = ApiClient.Post(request);
var valid = response.StatusCode == HttpStatusCode.OK;
if (valid)
Logger.Trace("Token is valid!");
Logger?.LogTrace("Token is valid!");
else
Logger.Trace("Token is invalid!");
Logger?.LogTrace("Token is invalid!");
return response.StatusCode == HttpStatusCode.OK;
}
public void UseToken(AuthToken token)
{
Token = token;
}
public void UseServerAddress(string address)
{
BaseUrl = address;
ApiClient = new RestClient(BaseUrl);
}
public IEnumerable<Game> GetGames()
{
return GetRequest<IEnumerable<Game>>("/api/Games");
@ -206,6 +267,11 @@ namespace LANCommander.PlaynitePlugin
return DownloadRequest($"/api/Archives/Download/{id}", progressHandler, completeHandler);
}
public TrackableStream StreamRedistributable(Guid id)
{
return StreamRequest($"/api/Redistributables/{id}/Download");
}
public string DownloadSave(Guid id, Action<DownloadProgressChangedEventArgs> progressHandler, Action<AsyncCompletedEventArgs> completeHandler)
{
return DownloadRequest($"/api/Saves/Download/{id}", progressHandler, completeHandler);
@ -218,21 +284,26 @@ namespace LANCommander.PlaynitePlugin
public GameSave UploadSave(string gameId, byte[] data)
{
Logger.Trace("Uploading save...");
Logger?.LogTrace("Uploading save...");
var request = new RestRequest($"/api/Saves/Upload/{gameId}", Method.POST)
.AddHeader("Authorization", $"Bearer {Token.AccessToken}");
request.AddFile(gameId, data, gameId);
var response = Client.Post<GameSave>(request);
var response = ApiClient.Post<GameSave>(request);
return response.Data;
}
public string GetMediaUrl(Media media)
{
return (new Uri(ApiClient.BaseUrl, $"/api/Media/{media.Id}/Download?fileId={media.FileId}").ToString());
}
public string GetKey(Guid id)
{
Logger.Trace("Requesting key allocation...");
Logger?.LogTrace("Requesting key allocation...");
var macAddress = GetMacAddress();
@ -251,7 +322,7 @@ namespace LANCommander.PlaynitePlugin
public string GetAllocatedKey(Guid id)
{
Logger.Trace("Requesting allocated key...");
Logger?.LogTrace("Requesting allocated key...");
var macAddress = GetMacAddress();
@ -273,7 +344,7 @@ namespace LANCommander.PlaynitePlugin
public string GetNewKey(Guid id)
{
Logger.Trace("Requesting new key allocation...");
Logger?.LogTrace("Requesting new key allocation...");
var macAddress = GetMacAddress();
@ -293,6 +364,36 @@ namespace LANCommander.PlaynitePlugin
return response.Value;
}
public User GetProfile()
{
Logger?.LogTrace("Requesting player's profile...");
return GetRequest<User>("/api/Profile");
}
public string ChangeAlias(string alias)
{
Logger?.LogTrace("Requesting to change player alias...");
var response = PostRequest<object>("/api/Profile/ChangeAlias", alias);
return alias;
}
public void StartPlaySession(Guid gameId)
{
Logger?.LogTrace("Starting a game session...");
PostRequest<object>($"/api/PlaySessions/Start/{gameId}");
}
public void EndPlaySession(Guid gameId)
{
Logger?.LogTrace("Ending a game session...");
PostRequest<object>($"/api/PlaySessions/End/{gameId}");
}
private string GetMacAddress()
{
return NetworkInterface.GetAllNetworkInterfaces()

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.Enums
{
public enum MediaType
{
Icon,
Cover,
Background
}
}

View File

@ -5,6 +5,9 @@
Install,
Uninstall,
NameChange,
KeyChange
KeyChange,
SaveUpload,
SaveDownload,
DetectInstall
}
}

View File

@ -0,0 +1,20 @@
using SharpCompress.Common;
using SharpCompress.Readers;
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK
{
public class ArchiveExtractionProgressArgs : EventArgs
{
public long Position { get; set; }
public long Length { get; set; }
}
public class ArchiveEntryExtractionProgressArgs : EventArgs
{
public ReaderProgress Progress { get; set; }
public IEntry Entry { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LANCommander.SDK
{
internal class ExtractionResult
{
public bool Success { get; set; }
public bool Canceled { get; set; }
public string Directory { get; set; }
}
}

View File

@ -0,0 +1,227 @@
using LANCommander.SDK.Enums;
using LANCommander.SDK.Extensions;
using LANCommander.SDK.Helpers;
using LANCommander.SDK.Models;
using Microsoft.Extensions.Logging;
using SharpCompress.Common;
using SharpCompress.Readers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace LANCommander.SDK
{
public class GameManager
{
private readonly ILogger Logger;
private Client Client { get; set; }
private string DefaultInstallDirectory { get; set; }
public delegate void OnArchiveEntryExtractionProgressHandler(object sender, ArchiveEntryExtractionProgressArgs e);
public event OnArchiveEntryExtractionProgressHandler OnArchiveEntryExtractionProgress;
public delegate void OnArchiveExtractionProgressHandler(long position, long length);
public event OnArchiveExtractionProgressHandler OnArchiveExtractionProgress;
private TrackableStream Stream;
private IReader Reader;
public GameManager(Client client, string defaultInstallDirectory)
{
Client = client;
DefaultInstallDirectory = defaultInstallDirectory;
}
public GameManager(Client client, string defaultInstallDirectory, ILogger logger)
{
Client = client;
DefaultInstallDirectory = defaultInstallDirectory;
Logger = logger;
}
/// <summary>
/// Downloads, extracts, and runs post-install scripts for the specified game
/// </summary>
/// <param name="game">Game to install</param>
/// <param name="maxAttempts">Maximum attempts in case of transmission error</param>
/// <returns>Final install path</returns>
/// <exception cref="Exception"></exception>
public string Install(Guid gameId, int maxAttempts = 10)
{
GameManifest manifest = null;
var game = Client.GetGame(gameId);
var destination = Path.Combine(DefaultInstallDirectory, game.Title.SanitizeFilename());
try
{
if (ManifestHelper.Exists(destination))
manifest = ManifestHelper.Read(destination);
}
catch (Exception ex)
{
Logger?.LogTrace(ex, "Error reading manifest before install");
}
if (manifest == null || manifest.Id != gameId)
{
Logger?.LogTrace("Installing game {GameTitle} ({GameId})", game.Title, game.Id);
var result = RetryHelper.RetryOnException<ExtractionResult>(maxAttempts, TimeSpan.FromMilliseconds(500), new ExtractionResult(), () =>
{
Logger?.LogTrace("Attempting to download and extract game");
return DownloadAndExtract(game, destination);
});
if (!result.Success && !result.Canceled)
throw new Exception("Could not extract the installer. Retry the install or check your connection");
else if (result.Canceled)
return "";
game.InstallDirectory = result.Directory;
}
else
{
Logger?.LogTrace("Game {GameTitle} ({GameId}) is already installed to {InstallDirectory}", game.Title, game.Id, destination);
game.InstallDirectory = destination;
}
var writeManifestSuccess = RetryHelper.RetryOnException(maxAttempts, TimeSpan.FromSeconds(1), false, () =>
{
Logger?.LogTrace("Attempting to get game manifest");
manifest = Client.GetGameManifest(game.Id);
ManifestHelper.Write(manifest, game.InstallDirectory);
return true;
});
if (!writeManifestSuccess)
throw new Exception("Could not grab the manifest file. Retry the install or check your connection");
Logger?.LogTrace("Saving scripts");
ScriptHelper.SaveScript(game, ScriptType.Install);
ScriptHelper.SaveScript(game, ScriptType.Uninstall);
ScriptHelper.SaveScript(game, ScriptType.NameChange);
ScriptHelper.SaveScript(game, ScriptType.KeyChange);
return game.InstallDirectory;
}
public void Uninstall(string installDirectory)
{
Logger?.LogTrace("Attempting to delete the install directory");
if (Directory.Exists(installDirectory))
Directory.Delete(installDirectory, true);
Logger?.LogTrace("Deleted install directory {InstallDirectory}", installDirectory);
}
private ExtractionResult DownloadAndExtract(Game game, string destination)
{
if (game == null)
{
Logger?.LogTrace("Game failed to download, no game was specified");
throw new ArgumentNullException("No game was specified");
}
Logger?.LogTrace("Downloading and extracting {Game} to path {Destination}", game.Title, destination);
var extractionResult = new ExtractionResult
{
Canceled = false,
};
try
{
Directory.CreateDirectory(destination);
Stream = Client.StreamGame(game.Id);
Reader = ReaderFactory.Open(Stream);
Stream.OnProgress += (pos, len) =>
{
OnArchiveExtractionProgress?.Invoke(pos, len);
};
Reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs<IEntry> e) =>
{
OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs
{
Entry = e.Item,
Progress = e.ReaderProgress,
});
};
while (Reader.MoveToNextEntry())
{
if (Reader.Cancelled)
break;
Reader.WriteEntryToDirectory(destination, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true,
PreserveFileTime = true,
});
}
Reader.Dispose();
Stream.Dispose();
}
catch (ReaderCancelledException ex)
{
Logger?.LogTrace("User cancelled the download");
extractionResult.Canceled = true;
if (Directory.Exists(destination))
{
Logger?.LogTrace("Cleaning up orphaned files after cancelled install");
Directory.Delete(destination, true);
}
}
catch (Exception ex)
{
Logger?.LogError(ex, "Could not extract to path {Destination}", destination);
if (Directory.Exists(destination))
{
Logger?.LogTrace("Cleaning up orphaned install files after bad install");
Directory.Delete(destination, true);
}
throw new Exception("The game archive could not be extracted, is it corrupted? Please try again");
}
if (!extractionResult.Canceled)
{
extractionResult.Success = true;
extractionResult.Directory = destination;
Logger?.LogTrace("Game {Game} successfully downloaded and extracted to {Destination}", game.Title, destination);
}
return extractionResult;
}
public void CancelInstall()
{
Reader?.Cancel();
// Reader?.Dispose();
// Stream?.Dispose();
}
}
}

View File

@ -0,0 +1,285 @@
using LANCommander.SDK;
using LANCommander.SDK.Helpers;
using LANCommander.SDK.Models;
using LANCommander.SDK.PowerShell;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Readers;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace LANCommander.SDK
{
public class GameSaveManager
{
private readonly Client Client;
public delegate void OnDownloadProgressHandler(DownloadProgressChangedEventArgs e);
public event OnDownloadProgressHandler OnDownloadProgress;
public delegate void OnDownloadCompleteHandler(AsyncCompletedEventArgs e);
public event OnDownloadCompleteHandler OnDownloadComplete;
public GameSaveManager(Client client)
{
Client = client;
}
public void Download(string installDirectory)
{
var manifest = ManifestHelper.Read(installDirectory);
string tempFile = String.Empty;
if (manifest != null)
{
var destination = Client.DownloadLatestSave(manifest.Id, (changed) =>
{
OnDownloadProgress?.Invoke(changed);
}, (complete) =>
{
OnDownloadComplete?.Invoke(complete);
});
tempFile = destination;
// Go into the archive and extract the files to the correct locations
try
{
var tempLocation = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempLocation);
ExtractFilesFromZip(tempFile, tempLocation);
#region Move files
foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "File"))
{
bool inInstallDir = savePath.Path.StartsWith("{InstallDir}");
string tempSavePath = Path.Combine(tempLocation, savePath.Id.ToString());
foreach (var entry in savePath.Entries)
{
var tempSavePathFile = Path.Combine(tempSavePath, entry.ArchivePath);
destination = Environment.ExpandEnvironmentVariables(entry.ActualPath).Replace("{InstallDir}", installDirectory);
if (File.Exists(tempSavePathFile))
{
if (File.Exists(destination))
File.Delete(destination);
File.Move(tempSavePathFile, destination);
}
else if (Directory.Exists(tempSavePath))
{
var files = Directory.GetFiles(tempSavePath, "*", SearchOption.AllDirectories);
foreach (var file in files)
{
if (inInstallDir)
{
// Files are in the game's install directory. Move them there from the save path.
destination = file.Replace(tempSavePath, savePath.Path.Replace('/', Path.DirectorySeparatorChar).TrimEnd(Path.DirectorySeparatorChar).Replace("{InstallDir}", installDirectory));
if (File.Exists(destination))
File.Delete(destination);
File.Move(file, destination);
}
else
{
// Specified path is probably an absolute path, maybe with environment variables.
destination = Environment.ExpandEnvironmentVariables(file.Replace(tempSavePathFile, savePath.Path.Replace('/', Path.DirectorySeparatorChar)));
if (File.Exists(destination))
File.Delete(destination);
File.Move(file, destination);
}
}
}
}
}
#endregion
#region Handle registry importing
var registryImportFilePath = Path.Combine(tempLocation, "_registry.reg");
if (File.Exists(registryImportFilePath))
{
var registryImportFileContents = File.ReadAllText(registryImportFilePath);
var script = new PowerShellScript();
script.UseInline($"regedit.exe /s \"{registryImportFilePath}\"");
if (registryImportFileContents.Contains("HKEY_LOCAL_MACHINE"))
script.RunAsAdmin();
script.Execute();
}
#endregion
// Clean up temp files
Directory.Delete(tempLocation, true);
}
catch (Exception ex)
{
}
}
}
public void Upload(string installDirectory)
{
var manifest = ManifestHelper.Read(installDirectory);
var temp = Path.GetTempFileName();
if (manifest.SavePaths != null && manifest.SavePaths.Count() > 0)
{
using (var archive = ZipArchive.Create())
{
archive.DeflateCompressionLevel = SharpCompress.Compressors.Deflate.CompressionLevel.BestCompression;
#region Add files from defined paths
foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "File"))
{
IEnumerable<string> localPaths;
if (savePath.IsRegex)
{
var regex = new Regex(Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', '\\').Replace("{InstallDir}", installDirectory)));
localPaths = Directory.GetFiles(installDirectory, "*", SearchOption.AllDirectories)
.Where(p => regex.IsMatch(p))
.ToList();
}
else
localPaths = new string[] { savePath.Path };
var entries = new List<SavePathEntry>();
foreach (var localPath in localPaths)
{
var actualPath = Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', Path.DirectorySeparatorChar).Replace("{InstallDir}", installDirectory));
var relativePath = actualPath.Replace(installDirectory + Path.DirectorySeparatorChar, "");
if (Directory.Exists(actualPath))
{
AddDirectoryToZip(archive, relativePath, actualPath, savePath.Id);
}
else if (File.Exists(actualPath))
{
archive.AddEntry(Path.Combine(savePath.Id.ToString(), relativePath), actualPath);
}
entries.Add(new SavePathEntry
{
ArchivePath = relativePath,
ActualPath = actualPath.Replace(installDirectory, "{InstallDir}")
});
savePath.Entries = entries;
}
}
#endregion
#region Export registry keys
if (manifest.SavePaths.Any(sp => sp.Type == "Registry"))
{
List<string> tempRegFiles = new List<string>();
var exportCommand = new StringBuilder();
foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "Registry"))
{
var tempRegFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".reg");
exportCommand.AppendLine($"reg.exe export \"{savePath.Path.Replace(":\\", "\\")}\" \"{tempRegFile}\"");
tempRegFiles.Add(tempRegFile);
}
var script = new PowerShellScript();
script.UseInline(exportCommand.ToString());
script.Execute();
var exportFile = new StringBuilder();
foreach (var tempRegFile in tempRegFiles)
{
exportFile.AppendLine(File.ReadAllText(tempRegFile));
File.Delete(tempRegFile);
}
archive.AddEntry("_registry.reg", new MemoryStream(Encoding.UTF8.GetBytes(exportFile.ToString())), true);
}
#endregion
var tempManifest = Path.GetTempFileName();
File.WriteAllText(tempManifest, ManifestHelper.Serialize(manifest));
archive.AddEntry("_manifest.yml", tempManifest);
using (var ms = new MemoryStream())
{
archive.SaveTo(ms);
ms.Seek(0, SeekOrigin.Begin);
var save = Client.UploadSave(manifest.Id.ToString(), ms.ToArray());
}
}
}
}
private void AddDirectoryToZip(ZipArchive zipArchive, string path, string workingDirectory, Guid pathId)
{
foreach (var file in Directory.GetFiles(path))
{
// Oh man is this a hack. We should be removing only the working directory from the start,
// but we're making the assumption that the working dir put in actually prefixes the path.
// Also wtf, that Path.Combine is stripping the pathId out?
zipArchive.AddEntry(Path.Combine(pathId.ToString(), path.Substring(workingDirectory.Length), Path.GetFileName(file)), file);
}
foreach (var child in Directory.GetDirectories(path))
{
// See above
//ZipEntry entry = new ZipEntry(Path.Combine(pathId.ToString(), path.Substring(workingDirectory.Length), Path.GetFileName(path)));
//zipStream.PutNextEntry(entry);
//zipStream.CloseEntry();
AddDirectoryToZip(zipArchive, child, workingDirectory, pathId);
}
}
private void ExtractFilesFromZip(string zipPath, string destination)
{
using (var fs = File.OpenRead(zipPath))
using (var ts = new TrackableStream(fs))
using (var reader = ReaderFactory.Open(ts))
{
reader.WriteAllToDirectory(destination, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true
});
}
}
}
}

View File

@ -0,0 +1,73 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using YamlDotNet.Serialization.NamingConventions;
using YamlDotNet.Serialization;
namespace LANCommander.SDK.Helpers
{
public static class ManifestHelper
{
public static readonly ILogger Logger;
public const string ManifestFilename = "_manifest.yml";
public static bool Exists(string installDirectory)
{
var path = GetPath(installDirectory);
return File.Exists(path);
}
public static GameManifest Read(string installDirectory)
{
var source = GetPath(installDirectory);
var yaml = File.ReadAllText(source);
var deserializer = new DeserializerBuilder()
.WithNamingConvention(new PascalCaseNamingConvention())
.Build();
Logger?.LogTrace("Deserializing manifest");
var manifest = deserializer.Deserialize<GameManifest>(yaml);
return manifest;
}
public static string Write(GameManifest manifest, string installDirectory)
{
var destination = GetPath(installDirectory);
Logger?.LogTrace("Attempting to write manifest to path {Destination}", destination);
var yaml = Serialize(manifest);
Logger?.LogTrace("Writing manifest file");
File.WriteAllText(destination, yaml);
return destination;
}
public static string Serialize(GameManifest manifest)
{
var serializer = new SerializerBuilder()
.WithNamingConvention(new PascalCaseNamingConvention())
.Build();
Logger?.LogTrace("Serializing manifest");
var yaml = serializer.Serialize(manifest);
return yaml;
}
public static string GetPath(string installDirectory)
{
return Path.Combine(installDirectory, ManifestFilename);
}
}
}

View File

@ -1,15 +1,12 @@
using Playnite.SDK;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LANCommander.PlaynitePlugin.Helpers
namespace LANCommander.SDK.Helpers
{
internal static class RetryHelper
{
internal static readonly ILogger Logger = LogManager.GetLogger();
internal static readonly ILogger Logger;
internal static T RetryOnException<T>(int maxAttempts, TimeSpan delay, T @default, Func<T> action)
{
@ -19,14 +16,14 @@ namespace LANCommander.PlaynitePlugin.Helpers
{
try
{
Logger.Trace($"Attempt #{attempts + 1}/{maxAttempts}...");
Logger?.LogTrace($"Attempt #{attempts + 1}/{maxAttempts}...");
attempts++;
return action();
}
catch (Exception ex)
{
Logger.Error(ex, $"Attempt failed!");
Logger?.LogError(ex, $"Attempt failed!");
if (attempts >= maxAttempts)
return @default;

View File

@ -0,0 +1,78 @@
using LANCommander.SDK.Enums;
using LANCommander.SDK.Models;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace LANCommander.SDK.Helpers
{
public static class ScriptHelper
{
public static readonly ILogger Logger;
public static string SaveTempScript(Script script)
{
var tempPath = SaveTempScript(script.Contents);
Logger?.LogTrace("Wrote script {Script} to {Destination}", script.Name, tempPath);
return tempPath;
}
public static string SaveTempScript(string contents)
{
var tempPath = Path.GetTempFileName();
// PowerShell will only run scripts with the .ps1 file extension
File.Move(tempPath, tempPath + ".ps1");
tempPath = tempPath + ".ps1";
File.WriteAllText(tempPath, contents);
return tempPath;
}
public static void SaveScript(Game game, ScriptType type)
{
var script = game.Scripts.FirstOrDefault(s => s.Type == type);
if (script == null)
return;
if (script.RequiresAdmin)
script.Contents = "# Requires Admin" + "\r\n\r\n" + script.Contents;
var filename = GetScriptFilePath(game, type);
if (File.Exists(filename))
File.Delete(filename);
Logger?.LogTrace("Writing {ScriptType} script to {Destination}", type, filename);
File.WriteAllText(filename, script.Contents);
}
public static string GetScriptFilePath(Game game, ScriptType type)
{
return GetScriptFilePath(game.InstallDirectory, type);
}
public static string GetScriptFilePath(string installDirectory, ScriptType type)
{
Dictionary<ScriptType, string> filenames = new Dictionary<ScriptType, string>() {
{ ScriptType.Install, "_install.ps1" },
{ ScriptType.Uninstall, "_uninstall.ps1" },
{ ScriptType.NameChange, "_changename.ps1" },
{ ScriptType.KeyChange, "_changekey.ps1" }
};
var filename = filenames[type];
return Path.Combine(installDirectory, filename);
}
}
}

View File

@ -1,7 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="PowerShellStandard.Library" Version="5.1.1" />
<PackageReference Include="RestSharp" Version="106.15.0" />
<PackageReference Include="SharpCompress" Version="0.34.2" />
<PackageReference Include="YamlDotNet" Version="5.4.0" />
</ItemGroup>
</Project>

View File

@ -8,5 +8,6 @@ namespace LANCommander.SDK.Models
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public DateTime Expiration { get; set; }
}
}

View File

@ -10,11 +10,14 @@ namespace LANCommander.SDK.Models
public string DirectoryName { get; set; }
public string Description { get; set; }
public DateTime ReleasedOn { get; set; }
public string InstallDirectory { get; set; }
public virtual IEnumerable<Action> Actions { get; set; }
public virtual IEnumerable<Tag> Tags { get; set; }
public virtual Company Publisher { get; set; }
public virtual Company Developer { get; set; }
public virtual IEnumerable<Archive> Archives { get; set; }
public virtual IEnumerable<Script> Scripts { get; set; }
public virtual IEnumerable<Media> Media { get; set; }
public virtual IEnumerable<Redistributable> Redistributables { get; set; }
}
}

View File

@ -6,6 +6,7 @@ namespace LANCommander.SDK
{
public class GameManifest
{
public Guid Id { get; set; }
public string Title { get; set; }
public string SortTitle { get; set; }
public string Description { get; set; }
@ -15,7 +16,6 @@ namespace LANCommander.SDK
public IEnumerable<string> Publishers { get; set; }
public IEnumerable<string> Developers { get; set; }
public string Version { get; set; }
public string Icon { get; set; }
public IEnumerable<GameAction> Actions { get; set; }
public bool Singleplayer { get; set; }
public MultiplayerInfo LocalMultiplayer { get; set; }
@ -47,5 +47,13 @@ namespace LANCommander.SDK
public Guid Id { get; set; }
public string Type { get; set; }
public string Path { get; set; }
public bool IsRegex { get; set; }
public IEnumerable<SavePathEntry> Entries { get; set; }
}
public class SavePathEntry
{
public string ArchivePath { get; set; }
public string ActualPath { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using LANCommander.SDK.Enums;
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.Models
{
public class Media : BaseModel
{
public Guid FileId { get; set; }
public MediaType Type { get; set; }
public string SourceUrl { get; set; }
public string MimeType { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
namespace LANCommander.SDK.Models
{
public class Redistributable : BaseModel
{
public string Name { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
public DateTime ReleasedOn { get; set; }
public virtual IEnumerable<Archive> Archives { get; set; }
public virtual IEnumerable<Script> Scripts { get; set; }
}
}

View File

@ -9,6 +9,7 @@ namespace LANCommander.SDK.Models
{
public SavePathType Type { get; set; }
public string Path { get; set; }
public bool IsRegex { get; set; }
public virtual Game Game { get; set; }
}
}

View File

@ -6,5 +6,6 @@ namespace LANCommander.SDK.Models
{
public Guid Id { get; set; }
public string UserName { get; set; }
public string Alias { get; set; }
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.PowerShell
{
public class PowerShellArgument
{
public string Name { get; set; }
public object Value { get; set; }
public Type Type { get; set; }
public PowerShellArgument(string name, object value, Type type)
{
Name = name;
Value = value;
Type = type;
}
}
}

View File

@ -0,0 +1,193 @@
using LANCommander.SDK.Helpers;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
namespace LANCommander.SDK.PowerShell
{
public class PowerShellScript
{
private string Contents { get; set; } = "";
private string WorkingDirectory { get; set; } = "";
private bool AsAdmin { get; set; } = false;
private bool ShellExecute { get; set; } = false;
private bool IgnoreWow64 { get; set; } = false;
private ICollection<PowerShellVariable> Variables { get; set; }
private Dictionary<string, string> Arguments { get; set; }
private List<string> Modules { get; set; }
private Process Process { get; set; }
public PowerShellScript()
{
Variables = new List<PowerShellVariable>();
Arguments = new Dictionary<string, string>();
Modules = new List<string>();
Process = new Process();
Process.StartInfo.FileName = "powershell.exe";
Process.StartInfo.RedirectStandardOutput = false;
AddArgument("ExecutionPolicy", "Unrestricted");
var moduleManifests = Directory.EnumerateFiles(Environment.CurrentDirectory, "LANCommander.PowerShell.psd1", SearchOption.AllDirectories);
if (moduleManifests.Any())
AddModule(moduleManifests.First());
IgnoreWow64Redirection();
}
public PowerShellScript UseFile(string path)
{
Contents = File.ReadAllText(path);
return this;
}
public PowerShellScript UseInline(string contents)
{
Contents = contents;
return this;
}
public PowerShellScript UseWorkingDirectory(string path)
{
WorkingDirectory = path;
return this;
}
public PowerShellScript UseShellExecute()
{
ShellExecute = true;
return this;
}
public PowerShellScript AddVariable<T>(string name, T value)
{
Variables.Add(new PowerShellVariable(name, value, typeof(T)));
return this;
}
public PowerShellScript AddArgument<T>(string name, T value)
{
Arguments.Add(name, $"\"{value}\"");
return this;
}
public PowerShellScript AddArgument(string name, int value)
{
Arguments[name] = value.ToString();
return this;
}
public PowerShellScript AddArgument(string name, long value)
{
Arguments[name] = value.ToString();
return this;
}
public PowerShellScript AddModule(string path)
{
Modules.Add(path);
return this;
}
public PowerShellScript RunAsAdmin()
{
AsAdmin = true;
Process.StartInfo.Verb = "runas";
Process.StartInfo.UseShellExecute = true;
return this;
}
public PowerShellScript IgnoreWow64Redirection()
{
IgnoreWow64 = true;
return this;
}
public int Execute()
{
var scriptBuilder = new StringBuilder();
var wow64Value = IntPtr.Zero;
if (Contents.StartsWith("# Requires Admin"))
RunAsAdmin();
foreach (var module in Modules)
{
scriptBuilder.AppendLine($"Import-Module \"{module}\"");
}
foreach (var variable in Variables)
{
scriptBuilder.AppendLine($"${variable.Name} = ConvertFrom-SerializedBase64 \"{Serialize(variable.Value)}\"");
}
scriptBuilder.AppendLine(Contents);
var path = ScriptHelper.SaveTempScript(scriptBuilder.ToString());
AddArgument("File", path);
if (IgnoreWow64)
Wow64DisableWow64FsRedirection(ref wow64Value);
foreach (var argument in Arguments)
{
Process.StartInfo.Arguments += $" -{argument.Key} {argument.Value}";
}
if (!String.IsNullOrEmpty(WorkingDirectory))
Process.StartInfo.WorkingDirectory = WorkingDirectory;
if (ShellExecute)
Process.StartInfo.UseShellExecute = true;
if (AsAdmin)
{
Process.StartInfo.Verb = "runas";
Process.StartInfo.UseShellExecute = true;
}
Process.Start();
Process.WaitForExit();
if (IgnoreWow64)
Wow64RevertWow64FsRedirection(ref wow64Value);
if (File.Exists(path))
File.Delete(path);
return Process.ExitCode;
}
public static string Serialize<T>(T input)
{
// Use the PowerShell serializer to generate XML for our input. Then convert to base64 so we can put it on one line.
return Convert.ToBase64String(Encoding.UTF8.GetBytes(System.Management.Automation.PSSerializer.Serialize(input)));
}
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool Wow64DisableWow64FsRedirection(ref IntPtr ptr);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool Wow64RevertWow64FsRedirection(ref IntPtr ptr);
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.PowerShell
{
public class PowerShellVariable
{
public string Name { get; set; }
public object Value { get; set; }
public Type Type { get; set; }
public PowerShellVariable(string name, object value, Type type)
{
Name = name;
Value = value;
Type = type;
}
}
}

View File

@ -0,0 +1,184 @@
using LANCommander.SDK.Enums;
using LANCommander.SDK.Extensions;
using LANCommander.SDK.Helpers;
using LANCommander.SDK.Models;
using LANCommander.SDK.PowerShell;
using Microsoft.Extensions.Logging;
using SharpCompress.Common;
using SharpCompress.Readers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace LANCommander.SDK
{
public class RedistributableManager
{
private readonly ILogger Logger;
private Client Client { get; set; }
public delegate void OnArchiveEntryExtractionProgressHandler(object sender, ArchiveEntryExtractionProgressArgs e);
public event OnArchiveEntryExtractionProgressHandler OnArchiveEntryExtractionProgress;
public delegate void OnArchiveExtractionProgressHandler(long position, long length);
public event OnArchiveExtractionProgressHandler OnArchiveExtractionProgress;
public RedistributableManager(Client client)
{
Client = client;
}
public RedistributableManager(Client client, ILogger logger)
{
Client = client;
Logger = logger;
}
public void Install(Game game)
{
foreach (var redistributable in game.Redistributables)
{
Install(redistributable);
}
}
public void Install(Redistributable redistributable)
{
string installScriptTempFile = null;
string detectionScriptTempFile = null;
string extractTempPath = null;
try
{
var installScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.Install);
installScriptTempFile = ScriptHelper.SaveTempScript(installScript);
var detectionScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.DetectInstall);
detectionScriptTempFile = ScriptHelper.SaveTempScript(detectionScript);
var detectionResult = RunScript(detectionScriptTempFile, redistributable, detectionScript.RequiresAdmin);
// Redistributable is not installed
if (detectionResult == 0)
{
if (redistributable.Archives.Count() > 0)
{
var extractionResult = DownloadAndExtract(redistributable);
if (extractionResult.Success)
{
extractTempPath = extractionResult.Directory;
RunScript(installScriptTempFile, redistributable, installScript.RequiresAdmin, extractTempPath);
}
}
else
{
RunScript(installScriptTempFile, redistributable, installScript.RequiresAdmin, extractTempPath);
}
}
}
catch (Exception ex)
{
Logger?.LogError(ex, "Redistributable {Redistributable} failed to install", redistributable.Name);
}
finally
{
if (File.Exists(installScriptTempFile))
File.Delete(installScriptTempFile);
if (File.Exists(detectionScriptTempFile))
File.Delete(detectionScriptTempFile);
if (Directory.Exists(extractTempPath))
Directory.Delete(extractTempPath);
}
}
private ExtractionResult DownloadAndExtract(Redistributable redistributable)
{
if (redistributable == null)
{
Logger?.LogTrace("Redistributable failed to download! No redistributable was specified");
throw new ArgumentNullException("No redistributable was specified");
}
var destination = Path.Combine(Path.GetTempPath(), redistributable.Name.SanitizeFilename());
Logger?.LogTrace("Downloading and extracting {Redistributable} to path {Destination}", redistributable.Name, destination);
try
{
Directory.CreateDirectory(destination);
using (var redistributableStream = Client.StreamRedistributable(redistributable.Id))
using (var reader = ReaderFactory.Open(redistributableStream))
{
redistributableStream.OnProgress += (pos, len) =>
{
OnArchiveExtractionProgress?.Invoke(pos, len);
};
reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs<IEntry> e) =>
{
OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs
{
Entry = e.Item,
Progress = e.ReaderProgress,
});
};
reader.WriteAllToDirectory(destination, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true
});
}
}
catch (Exception ex)
{
Logger?.LogError(ex, "Could not extract to path {Destination}", destination);
if (Directory.Exists(destination))
{
Logger?.LogTrace("Cleaning up orphaned files after bad install");
Directory.Delete(destination, true);
}
throw new Exception("The redistributable archive could not be extracted, is it corrupted? Please try again");
}
var extractionResult = new ExtractionResult
{
Canceled = false
};
if (!extractionResult.Canceled)
{
extractionResult.Success = true;
extractionResult.Directory = destination;
Logger?.LogTrace("Redistributable {Redistributable} successfully downloaded and extracted to {Destination}", redistributable.Name, destination);
}
return extractionResult;
}
private int RunScript(string path, Redistributable redistributable, bool requiresAdmin = false, string workingDirectory = "")
{
var script = new PowerShellScript();
script.AddVariable("Redistributable", redistributable);
script.UseWorkingDirectory(workingDirectory);
script.UseFile(path);
if (requiresAdmin)
script.RunAsAdmin();
return script.Execute();
}
}
}

View File

@ -1,9 +1,9 @@
using System;
using System.IO;
namespace LANCommander.PlaynitePlugin
namespace LANCommander.SDK
{
internal class TrackableStream : MemoryStream, IDisposable
public class TrackableStream : MemoryStream, IDisposable
{
public delegate void OnProgressDelegate(long Position, long Length);
public event OnProgressDelegate OnProgress = delegate { };

View File

@ -11,6 +11,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LANCommander.SDK", "LANComm
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LANCommander.PCGamingWiki", "LANCommander.PCGamingWiki\LANCommander.PCGamingWiki.csproj", "{2436B817-4475-4E70-9BB2-E1E7866DB79F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LANCommander.PowerShell", "LANCommander.PowerShell\LANCommander.PowerShell.csproj", "{807943BF-0C7D-4ED3-8393-CFEE64E3138C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LANCommander.PowerShell.Tests", "LANCommander.PowerShell.Tests\LANCommander.PowerShell.Tests.csproj", "{D7069A13-F0AA-4CBF-9013-4276F130A6DD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -33,6 +37,14 @@ Global
{2436B817-4475-4E70-9BB2-E1E7866DB79F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2436B817-4475-4E70-9BB2-E1E7866DB79F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2436B817-4475-4E70-9BB2-E1E7866DB79F}.Release|Any CPU.Build.0 = Release|Any CPU
{807943BF-0C7D-4ED3-8393-CFEE64E3138C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{807943BF-0C7D-4ED3-8393-CFEE64E3138C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{807943BF-0C7D-4ED3-8393-CFEE64E3138C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{807943BF-0C7D-4ED3-8393-CFEE64E3138C}.Release|Any CPU.Build.0 = Release|Any CPU
{D7069A13-F0AA-4CBF-9013-4276F130A6DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D7069A13-F0AA-4CBF-9013-4276F130A6DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D7069A13-F0AA-4CBF-9013-4276F130A6DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D7069A13-F0AA-4CBF-9013-4276F130A6DD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -1,4 +1,6 @@
@page
@using LANCommander.Models;
@using LANCommander.Services;
@model FirstTimeSetupModel
@{
Layout = "/Views/Shared/_LayoutBasic.cshtml";
@ -11,7 +13,16 @@
<div class="ant-col ant-col-xs-24 ant-col-md-10">
<div style="text-align: center; margin-bottom: 24px;">
<img src="~/static/logo.svg" />
@switch (SettingService.GetSettings().Theme)
{
case LANCommanderTheme.Light:
<img src="~/static/logo.svg" />
break;
case LANCommanderTheme.Dark:
<img src="~/static/logo-dark.svg" />
break;
}
</div>
<div class="ant-card ant-card-bordered">

View File

@ -1,4 +1,6 @@
@page
@using LANCommander.Models;
@using LANCommander.Services;
@model LoginModel
@{ Layout = "/Views/Shared/_LayoutBasic.cshtml"; }
@ -10,7 +12,16 @@
<div class="ant-col ant-col-xs-24 ant-col-md-10">
<div style="text-align: center; margin-bottom: 24px;">
<img src="~/static/logo.svg" />
@switch (SettingService.GetSettings().Theme)
{
case LANCommanderTheme.Light:
<img src="~/static/logo.svg" />
break;
case LANCommanderTheme.Dark:
<img src="~/static/logo-dark.svg" />
break;
}
</div>
@foreach (var error in ModelState.SelectMany(x => x.Value.Errors))

View File

@ -15,29 +15,20 @@ namespace LANCommander.Areas.Identity.Pages.Account
{
public class LogoutModel : PageModel
{
private readonly SignInManager<User> _signInManager;
private readonly ILogger<LogoutModel> _logger;
private readonly SignInManager<User> SignInManager;
private readonly ILogger<LogoutModel> Logger;
public LogoutModel(SignInManager<User> signInManager, ILogger<LogoutModel> logger)
{
_signInManager = signInManager;
_logger = logger;
SignInManager = signInManager;
Logger = logger;
}
public async Task<IActionResult> OnPost(string returnUrl = null)
public async Task<IActionResult> OnGet()
{
await _signInManager.SignOutAsync();
_logger.LogInformation("User logged out.");
if (returnUrl != null)
{
return LocalRedirect(returnUrl);
}
else
{
// This needs to be a redirect so that the browser performs a new
// request and the identity for the user gets updated.
return RedirectToPage();
}
await SignInManager.SignOutAsync();
return LocalRedirect("/");
}
}
}

View File

@ -1,4 +1,6 @@
@page
@using LANCommander.Models;
@using LANCommander.Services;
@model RegisterModel
@{ Layout = "/Views/Shared/_LayoutBasic.cshtml"; }
@{
@ -9,7 +11,16 @@
<div class="ant-col ant-col-xs-24 ant-col-md-10">
<div style="text-align: center; margin-bottom: 24px;">
<img src="~/static/logo.svg" />
@switch (SettingService.GetSettings().Theme)
{
case LANCommanderTheme.Light:
<img src="~/static/logo.svg" />
break;
case LANCommanderTheme.Dark:
<img src="~/static/logo-dark.svg" />
break;
}
</div>
<div class="ant-card ant-card-bordered">

View File

@ -1,232 +0,0 @@
@using AntDesign.TableModels;
@using ByteSizeLib;
@using LANCommander.Services;
@using System.IO.Compression;
@inject ArchiveService ArchiveService;
<GridRow Style="position: fixed; height: calc(100vh - 55px - 53px); top: 55px; left: 0; width: 100%">
<GridCol Span="6" Style="height: 100%; overflow-y: scroll; padding: 24px">
<Tree TItem="ArchiveDirectory"
DataSource="Directories"
TitleExpression="x => x.DataItem.Name"
ChildrenExpression="x => x.DataItem.Children"
IsLeafExpression="x => !x.DataItem.HasChildren"
OnClick="(args) => ChangeDirectory(args.Node.DataItem)">
</Tree>
</GridCol>
<GridCol Span="18" Style="height: 100%">
<Table
@ref="FileTable"
TItem="ZipArchiveEntry"
DataSource="CurrentPathEntries"
HidePagination="true"
Loading="Entries == null"
RowSelectable="@(x => CanSelect(x))"
OnRowClick="OnRowClicked"
SelectedRowsChanged="SelectedFilesChanged"
ScrollY="calc(100vh - 55px - 55px - 53px)">
@if (Select)
{
<Selection Key="@context.FullName" Type="@(Multiple ? "checkbox" : "radio")" Disabled="@(!CanSelect(context))" />
}
<Column TData="string" Width="32">
<Icon Type="@GetIcon(context)" Theme="outline" />
</Column>
<PropertyColumn Property="e => e.FullName" Sortable Title="Name">
@GetFileName(context)
</PropertyColumn>
<PropertyColumn Property="e => e.Length" Sortable Title="Size">
@ByteSize.FromBytes(context.Length)
</PropertyColumn>
<PropertyColumn Property="e => e.LastWriteTime" Format="MM/dd/yyyy hh:mm tt" Sortable Title="Modified" />
</Table>
</GridCol>
</GridRow>
<style>
.select-file-button {
opacity: 0;
transition: .1s opacity;
}
.archive-browser tr:hover .select-file-button {
opacity: 1;
}
</style>
@code {
[Parameter] public Guid ArchiveId { get; set; }
[Parameter] public bool Select { get; set; }
[Parameter] public bool Multiple { get; set; } = false;
[Parameter] public bool AllowDirectories { get; set; } = false;
[Parameter] public IEnumerable<ZipArchiveEntry> SelectedFiles { get; set; }
[Parameter] public EventCallback<IEnumerable<ZipArchiveEntry>> SelectedFilesChanged { get; set; }
ITable? FileTable;
private IEnumerable<ZipArchiveEntry> Entries { get; set; }
private IEnumerable<ZipArchiveEntry> CurrentPathEntries { get; set; }
private string CurrentPath { get; set; }
private HashSet<ArchiveDirectory> Directories { get; set; }
private ArchiveDirectory SelectedDirectory { get; set; }
protected override async Task OnInitializedAsync()
{
Entries = await ArchiveService.GetContents(ArchiveId);
Directories = new HashSet<ArchiveDirectory>();
var root = new ArchiveDirectory()
{
Name = "/",
FullName = "",
IsExpanded = true
};
root.PopulateChildren(Entries);
Directories.Add(root);
ChangeDirectory(root);
}
private void OnRowClicked(RowData<ZipArchiveEntry> row)
{
FileTable.SetSelection(new string[] { row.Data.FullName });
}
private void ChangeDirectory(ArchiveDirectory selectedDirectory)
{
SelectedDirectory = selectedDirectory;
if (SelectedDirectory.FullName == "")
CurrentPathEntries = Entries.Where(e => !e.FullName.TrimEnd('/').Contains('/'));
else
CurrentPathEntries = Entries.Where(e => e.FullName.StartsWith(SelectedDirectory.FullName) && e.FullName != SelectedDirectory.FullName);
}
private string GetFileName(ZipArchiveEntry entry)
{
if (String.IsNullOrWhiteSpace(entry.Name) && entry.Length == 0)
{
return entry.FullName.TrimEnd('/').Split('/').Last();
}
else
return entry.Name;
}
private string GetIcon(ZipArchiveEntry entry)
{
switch (Path.GetExtension(entry.FullName))
{
case "":
return "folder";
case ".exe":
return "code";
case ".zip":
case ".rar":
case ".7z":
case ".gz":
case ".tar":
return "file-zip";
case ".wad":
case ".pk3":
case ".pak":
case ".cab":
return "file-zip";
case ".txt":
case ".cfg":
case ".config":
case ".ini":
case ".yml":
case ".yaml":
case ".log":
case ".doc":
case ".nfo":
return "file-text";
case ".bat":
case ".ps1":
case ".json":
return "code";
case ".bik":
case ".avi":
case ".mov":
case ".mp4":
case ".m4v":
case ".mkv":
case ".wmv":
case ".mpg":
case ".mpeg":
case ".flv":
return "video-camera";
case ".dll":
return "api";
case ".hlp":
return "file-unknown";
case ".png":
case ".bmp":
case ".jpeg":
case ".jpg":
case ".gif":
return "file-image";
default:
return "file";
}
}
private bool CanSelect(ZipArchiveEntry entry)
{
if (entry == null || entry.FullName == null)
return false;
var isDirectory = entry.FullName.EndsWith('/');
if (isDirectory && AllowDirectories)
return true;
else if (!isDirectory)
return true;
else
return false;
}
public class ArchiveDirectory
{
public string Name { get; set; }
public string FullName { get; set; }
public bool IsExpanded { get; set; } = false;
public bool HasChildren => Children != null && Children.Count > 0;
public HashSet<ArchiveDirectory> Children { get; set; } = new HashSet<ArchiveDirectory>();
public void PopulateChildren(IEnumerable<ZipArchiveEntry> entries)
{
var childPaths = entries.Where(e => e.FullName.StartsWith(FullName) && e.FullName.EndsWith('/'));
var directChildren = childPaths.Where(p => p.FullName != FullName && p.FullName.Substring(FullName.Length + 1).TrimEnd('/').Split('/').Length == 1);
foreach (var directChild in directChildren)
{
var child = new ArchiveDirectory()
{
FullName = directChild.FullName,
Name = directChild.FullName.Substring(FullName.Length).TrimEnd('/')
};
child.PopulateChildren(entries);
Children.Add(child);
}
}
}
}

View File

@ -1,14 +0,0 @@
@inherits FeedbackComponent<ArchiveBrowserOptions, IEnumerable<ZipArchiveEntry>>
@using System.IO.Compression;
@using LANCommander.Models;
<ArchiveBrowser ArchiveId="Options.ArchiveId" @bind-SelectedFiles="SelectedFiles" Select="Options.Select" Multiple="Options.Multiple" AllowDirectories="Options.AllowDirectories" />
@code {
private IEnumerable<ZipArchiveEntry> SelectedFiles { get; set; }
public override async Task OnFeedbackOkAsync(ModalClosingEventArgs args)
{
await base.OkCancelRefWithResult!.OnOk(SelectedFiles);
}
}

View File

@ -11,7 +11,7 @@
<Space Direction="DirectionVHType.Vertical" Style="width: 100%">
<SpaceItem>
<Table TItem="Archive" DataSource="@Game.Archives.OrderByDescending(a => a.CreatedOn)" HidePagination="true" Responsive>
<Table TItem="Archive" DataSource="@Archives" HidePagination="true" Responsive>
<PropertyColumn Property="a => a.Version" />
<PropertyColumn Property="a => a.CompressedSize">
@ByteSizeLib.ByteSize.FromBytes(context.CompressedSize)
@ -19,11 +19,11 @@
<PropertyColumn Property="a => a.CreatedBy">
@context.CreatedBy?.UserName
</PropertyColumn>
<PropertyColumn Property="a => a.CreatedOn" Format="MM/dd/yyyy hh:mm tt" />
<PropertyColumn Property="a => a.CreatedOn" Format="MM/dd/yyyy hh:mm tt" DefaultSortOrder="@SortDirection.Descending" />
<ActionColumn Title="">
<Space Style="display: flex; justify-content: end">
<SpaceItem>
<a href="/Download/Game/@context.Id" target="_blank" class="ant-btn ant-btn-text ant-btn-icon-only">
<a href="/Download/Archive/@context.Id" target="_blank" class="ant-btn ant-btn-text ant-btn-icon-only">
<Icon Type="@IconType.Outline.Download" />
</a>
</SpaceItem>
@ -46,12 +46,14 @@
</SpaceItem>
</Space>
<ArchiveUploader @ref="Uploader" OnArchiveUploaded="AddArchive" />
<ArchiveUploader @ref="Uploader" GameId="GameId" RedistributableId="RedistributableId" OnArchiveUploaded="LoadData" />
@code {
[Parameter] public Game Game { get; set; }
[Parameter] public Guid GameId { get; set; }
[Parameter] public Guid RedistributableId { get; set; }
ICollection<Archive> Archives { get; set; }
Archive Archive;
ArchiveUploader Uploader;
protected override async Task OnInitializedAsync()
@ -63,10 +65,10 @@
private async Task LoadData()
{
Game.Archives = await ArchiveService.Get(a => a.GameId == Game.Id).OrderByDescending(a => a.CreatedOn).ToListAsync();
if (Game.Archives == null)
Game.Archives = new List<Archive>();
if (GameId != Guid.Empty)
Archives = await ArchiveService.Get(a => a.GameId == GameId).ToListAsync();
else if (RedistributableId != Guid.Empty)
Archives = await ArchiveService.Get(a => a.RedistributableId == RedistributableId).ToListAsync();
}
private async Task Download(Archive archive)
@ -78,25 +80,7 @@
private async Task UploadArchive()
{
Archive = new Archive()
{
GameId = Game.Id,
Id = Guid.NewGuid()
};
await Uploader.Open(Archive);
}
private async Task AddArchive(Archive archive)
{
var lastArchive = Game.Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault();
Archive = await ArchiveService.Add(archive);
await LoadData();
if (lastArchive != null)
BackgroundJob.Enqueue<PatchArchiveBackgroundJob>(x => x.Execute(lastArchive.Id, Archive.Id));
await Uploader.Open();
}
private async Task Delete(Archive archive)
@ -105,6 +89,8 @@
{
await ArchiveService.Delete(archive);
await LoadData();
await MessageService.Success("Archive deleted!");
}
catch (Exception ex)

View File

@ -2,6 +2,7 @@
@using System.Diagnostics;
@using Hangfire;
@using LANCommander.Jobs.Background;
@using Microsoft.EntityFrameworkCore;
@inject HttpClient HttpClient
@inject NavigationManager Navigator
@inject ArchiveService ArchiveService
@ -62,7 +63,9 @@
</Modal>
@code {
[Parameter] public EventCallback<Archive> OnArchiveUploaded { get; set; }
[Parameter] public Guid GameId { get; set; }
[Parameter] public Guid RedistributableId { get; set; }
[Parameter] public EventCallback<Guid> OnArchiveUploaded { get; set; }
Archive Archive;
@ -113,9 +116,21 @@
File = args.File;
}
public async Task Open(Archive archive)
public async Task Open(Guid? archiveId = null)
{
Archive = archive;
if (archiveId.HasValue && archiveId != Guid.Empty)
{
Archive = await ArchiveService.Get(archiveId.Value);
}
else
{
Archive = new Archive();
if (GameId != Guid.Empty)
Archive.GameId = GameId;
else if (RedistributableId != Guid.Empty)
Archive.RedistributableId = RedistributableId;
}
Visible = true;
@ -128,8 +143,8 @@
{
if (FileInput != null)
{
if (!String.IsNullOrWhiteSpace(archive.ObjectKey) && archive.ObjectKey != Guid.Empty.ToString())
await JS.InvokeVoidAsync("Uploader.Init", "FileInput", archive.ObjectKey.ToString());
if (!String.IsNullOrWhiteSpace(Archive.ObjectKey) && Archive.ObjectKey != Guid.Empty.ToString())
await JS.InvokeVoidAsync("Uploader.Init", "FileInput", Archive.ObjectKey.ToString());
else
await JS.InvokeVoidAsync("Uploader.Init", "FileInput", "");
@ -163,12 +178,32 @@
Archive.ObjectKey = objectKey.ToString();
Archive.CompressedSize = File.Size;
if (Archive.Id != Guid.Empty)
Archive = await ArchiveService.Update(Archive);
else
Archive = await ArchiveService.Add(Archive);
Visible = false;
await InvokeAsync(StateHasChanged);
Archive? lastArchive = null;
var settings = SettingService.GetSettings();
if (settings.Archives.EnablePatching)
{
if (Archive.GameId != Guid.Empty)
lastArchive = await ArchiveService.Get(a => a.Id != Archive.Id && a.GameId == Archive.GameId).OrderByDescending(a => a.CreatedOn).FirstOrDefaultAsync();
else if (Archive.RedistributableId != Guid.Empty)
lastArchive = await ArchiveService.Get(a => a.Id != Archive.Id && a.RedistributableId == Archive.RedistributableId).OrderByDescending(a => a.CreatedOn).FirstOrDefaultAsync();
if (lastArchive != null && settings.Archives.EnablePatching)
BackgroundJob.Enqueue<PatchArchiveBackgroundJob>(x => x.Execute(lastArchive.Id, Archive.Id));
}
if (OnArchiveUploaded.HasDelegate)
await OnArchiveUploaded.InvokeAsync(Archive);
await OnArchiveUploaded.InvokeAsync(Archive.Id);
await MessageService.Success("Archive uploaded!");
}

View File

@ -0,0 +1,522 @@
@using AntDesign.TableModels;
@using LANCommander.Components.FileManagerComponents
@inject ArchiveService ArchiveService
@inject IMessageService MessageService
@namespace LANCommander.Components
<div class="file-manager">
<GridRow Class="file-manager-nav">
<Space>
@if (Features.HasFlag(FileManagerFeatures.NavigationBack))
{
<SpaceItem>
<Tooltip Title="Back" MouseEnterDelay="2">
<Button Type="@ButtonType.Text" Icon="@IconType.Outline.ArrowLeft" OnClick="NavigateBack" Disabled="@(Past.Count == 0)" />
</Tooltip>
</SpaceItem>
}
@if (Features.HasFlag(FileManagerFeatures.NavigationForward))
{
<SpaceItem>
<Tooltip Title="Forward" MouseEnterDelay="2">
<Button Type="@ButtonType.Text" Icon="@IconType.Outline.ArrowRight" OnClick="NavigateForward" Disabled="@(Future.Count == 0)" />
</Tooltip>
</SpaceItem>
}
@if (Features.HasFlag(FileManagerFeatures.UpALevel))
{
<SpaceItem>
<Tooltip Title="Up a Level" MouseEnterDelay="2">
<Button Type="@ButtonType.Text" Icon="@IconType.Outline.ArrowUp" OnClick="NavigateUp" Disabled="@(Path.Parent == null)" />
</Tooltip>
</SpaceItem>
}
@if (Features.HasFlag(FileManagerFeatures.Refresh))
{
<SpaceItem>
<Tooltip Title="Refresh" MouseEnterDelay="2">
<Button Type="@ButtonType.Text" Icon="@IconType.Outline.Reload" OnClick="Refresh" />
</Tooltip>
</SpaceItem>
}
@if (Features.HasFlag(FileManagerFeatures.Breadcrumbs))
{
<SpaceItem Class="file-manager-nav-breadcrumbs">
<Breadcrumb>
@foreach (var breadcrumb in Breadcrumbs)
{
<BreadcrumbItem OnClick="() => ChangeDirectory(breadcrumb, false)">@breadcrumb.Name</BreadcrumbItem>
}
</Breadcrumb>
</SpaceItem>
}
@if (Features.HasFlag(FileManagerFeatures.NewFolder))
{
<SpaceItem>
<Tooltip Title="New Folder" MouseEnterDelay="2">
<Button Type="@ButtonType.Text" Icon="@IconType.Outline.FolderAdd" OnClick="() => NewFolderModal.Open()" />
</Tooltip>
</SpaceItem>
}
@if (Features.HasFlag(FileManagerFeatures.UploadFile))
{
<SpaceItem>
<Tooltip Title="Upload File" MouseEnterDelay="2">
<Button Type="@ButtonType.Text" Icon="@IconType.Outline.Upload" OnClick="() => UploadModal.Open()" />
</Tooltip>
</SpaceItem>
}
@if (Features.HasFlag(FileManagerFeatures.Delete))
{
<SpaceItem>
<Tooltip Title="Delete" MouseEnterDelay="2">
<Popconfirm OnConfirm="Delete">
<TitleTemplate>
Are you sure you want to delete the selected file@(Selected?.Count() == 1 ? "" : "s")?
</TitleTemplate>
<ChildContent>
<Button Type="@ButtonType.Text" Icon="@IconType.Outline.Delete" Disabled="@(Selected?.Count() == 0)" />
</ChildContent>
</Popconfirm>
</Tooltip>
</SpaceItem>
}
</Space>
</GridRow>
<GridRow Class="file-manager-body">
<GridCol Span="6" Class="file-manager-tree">
<Tree TItem="FileManagerDirectory"
DataSource="Directories"
SwitcherIcon="@IconType.Outline.Down"
TitleExpression="x => x.DataItem.Name"
ChildrenExpression="x => x.DataItem.Children"
IsLeafExpression="x => !x.DataItem.HasChildren"
IconExpression="x => x.Expanded ? IconType.Outline.FolderOpen : IconType.Outline.Folder"
DefaultExpandParent="true"
OnClick="(args) => ChangeDirectory(args.Node.DataItem, false)"
OnNodeLoadDelayAsync="ExpandTree">
<SwitcherIconTemplate>
<Icon Type="@IconType.Outline.Down" />
</SwitcherIconTemplate>
<TitleIconTemplate>
@if (context.Expanded)
{
<Icon Type="@IconType.Outline.FolderOpen" />
}
else
{
<Icon Type="@IconType.Outline.Folder" />
}
</TitleIconTemplate>
</Tree>
</GridCol>
<GridCol Span="18" Class="file-manager-list">
<Table TItem="IFileManagerEntry"
DataSource="Entries"
HidePagination="true"
Loading="Entries == null"
OnRow="OnRow"
SelectedRowsChanged="SelectedChanged"
RowSelectable="EntrySelectable"
Size="@TableSize.Small">
<Selection Key="@context.Path" Type="@(SelectMultiple ? "checkbox" : "radio")" Disabled="!EntrySelectable.Invoke(context)" Class="@(EntrySelectable.Invoke(context) ? "" : "file-manager-selector-hidden")" />
<Column TData="string" Width="32">
@if (context is FileManagerFile)
{
<Icon Type="@(((FileManagerFile)context).GetIcon())" Theme="outline" />
}
else if (context is FileManagerDirectory)
{
<Icon Type="@IconType.Outline.Folder" />
}
</Column>
<PropertyColumn Property="e => e.Path" Sortable Title="Name">
@GetEntryName(context)
</PropertyColumn>
<PropertyColumn Property="e => e.Size" Sortable Title="Size">
@ByteSizeLib.ByteSize.FromBytes(context.Size)
</PropertyColumn>
<PropertyColumn Property="e => e.ModifiedOn" Format="MM/dd/yyyy hh:mm tt" Sortable Title="Modified" />
</Table>
</GridCol>
</GridRow>
</div>
<NewFolderModal @ref="NewFolderModal" OnFolderNameEntered="AddFolder" />
<UploadModal @ref="UploadModal" Path="@Path.Path" OnUploadCompleted="() => Refresh()" />
@code {
[Parameter] public Guid ArchiveId { get; set; }
[Parameter] public string WorkingDirectory { get; set; }
[Parameter] public bool SelectMultiple { get; set; } = true;
[Parameter] public FileManagerFeatures Features { get; set; } = FileManagerFeatures.NavigationBack | FileManagerFeatures.NavigationForward | FileManagerFeatures.UpALevel | FileManagerFeatures.Refresh | FileManagerFeatures.Breadcrumbs | FileManagerFeatures.NewFolder | FileManagerFeatures.UploadFile | FileManagerFeatures.Delete;
[Parameter] public IEnumerable<IFileManagerEntry> Selected { get; set; } = new List<IFileManagerEntry>();
[Parameter] public EventCallback<IEnumerable<IFileManagerEntry>> SelectedChanged { get; set; }
[Parameter] public Func<IFileManagerEntry, bool> EntrySelectable { get; set; } = _ => true;
[Parameter] public Func<IFileManagerEntry, bool> EntryVisible { get; set; } = _ => true;
FileManagerSource Source = FileManagerSource.FileSystem;
FileManagerDirectory Path { get; set; } = new FileManagerDirectory();
List<FileManagerDirectory> Past { get; set; } = new List<FileManagerDirectory>();
List<FileManagerDirectory> Future { get; set; } = new List<FileManagerDirectory>();
List<FileManagerDirectory> Breadcrumbs = new List<FileManagerDirectory>();
List<IFileManagerEntry> Entries { get; set; } = new List<IFileManagerEntry>();
HashSet<FileManagerDirectory> Directories { get; set; } = new HashSet<FileManagerDirectory>();
NewFolderModal NewFolderModal;
UploadModal UploadModal;
Dictionary<string, object> OnRow(RowData<IFileManagerEntry> row) => new()
{
["data-path"] = row.Data.Path,
["ondblclick"] = ((System.Action)delegate
{
if (row.Data is FileManagerDirectory)
ChangeDirectory((FileManagerDirectory)row.Data, true);
})
};
protected override async Task OnInitializedAsync()
{
if (!String.IsNullOrWhiteSpace(WorkingDirectory))
Source = FileManagerSource.FileSystem;
else if (ArchiveId != Guid.Empty)
Source = FileManagerSource.Archive;
Directories = await GetDirectoriesAsync();
}
async Task<HashSet<FileManagerDirectory>> GetDirectoriesAsync()
{
switch (Source)
{
case FileManagerSource.FileSystem:
return await GetFileSystemDirectoriesAsync(WorkingDirectory);
case FileManagerSource.Archive:
return await GetArchiveDirectoriesAsync(ArchiveId);
}
return new HashSet<FileManagerDirectory>();
}
async Task<HashSet<FileManagerDirectory>> GetFileSystemDirectoriesAsync(string path)
{
var paths = Directory.EnumerateDirectories(path, "*", new EnumerationOptions
{
IgnoreInaccessible = true,
RecurseSubdirectories = true,
MaxRecursionDepth = 1
});
var root = new FileManagerDirectory
{
Name = path,
Path = path,
IsExpanded = true
};
root.PopulateChildren(paths);
await ChangeDirectory(root, true);
return new HashSet<FileManagerDirectory>
{
root
};
}
async Task<HashSet<FileManagerDirectory>> GetArchiveDirectoriesAsync(Guid archiveId)
{
try
{
var entries = await ArchiveService.GetContents(archiveId);
var directories = new HashSet<FileManagerDirectory>();
var root = new FileManagerDirectory
{
Name = "Root",
Path = "",
IsExpanded = true
};
root.PopulateChildren(entries);
await ChangeDirectory(root, true);
return new HashSet<FileManagerDirectory>
{
root
};
}
catch (FileNotFoundException ex)
{
MessageService.Error("Could not open archive! Is it missing?");
}
catch (Exception ex)
{
MessageService.Error("An unknown error occurred trying to open the archive");
}
return new HashSet<FileManagerDirectory>();
}
string GetEntryName(IFileManagerEntry entry)
{
if (String.IsNullOrWhiteSpace(entry.Name) && entry.Size == 0)
{
return entry.Path.TrimEnd('/').Split('/').Last();
}
else
return entry.Name;
}
async Task ChangeDirectory(FileManagerDirectory directory, bool clearFuture)
{
if (Path != null && !String.IsNullOrWhiteSpace(Path.Path) && directory.Path != Path.Path && Past.LastOrDefault()?.Path != directory.Path)
Past.Add(Path);
Path = directory;
await UpdateEntries();
UpdateBreadcrumbs();
if (clearFuture)
Future.Clear();
StateHasChanged();
}
async Task ExpandTree(TreeEventArgs<FileManagerDirectory> args)
{
if (Source == FileManagerSource.FileSystem)
{
var directory = (FileManagerDirectory)args.Node.DataItem;
foreach (var child in directory.Children)
{
await Task.Run(() =>
{
var paths = Directory.EnumerateDirectories(child.Path, "*", new EnumerationOptions
{
IgnoreInaccessible = true,
RecurseSubdirectories = true,
MaxRecursionDepth = 1
});
child.PopulateChildren(paths);
});
}
}
}
async Task UpdateEntries()
{
Entries.Clear();
switch (Source)
{
case FileManagerSource.FileSystem:
await Task.Run(UpdateFileSystemEntries);
break;
case FileManagerSource.Archive:
await UpdateArchiveEntries();
break;
}
}
void UpdateFileSystemEntries()
{
var entries = Directory.EnumerateFileSystemEntries(Path.Path);
var separator = System.IO.Path.DirectorySeparatorChar;
foreach (var entry in entries)
{
if (Directory.Exists(entry))
{
try
{
var info = new DirectoryInfo(entry);
var directory = new FileManagerDirectory
{
Path = entry,
Name = entry.Substring(Path.Path.Length).TrimStart(separator),
ModifiedOn = info.LastWriteTime,
CreatedOn = info.CreationTime,
Parent = Path
};
if (EntryVisible.Invoke(directory))
Entries.Add(directory);
}
catch { }
}
else
{
try
{
var info = new FileInfo(entry);
var file = new FileManagerFile
{
Path = entry,
Name = System.IO.Path.GetFileName(entry),
ModifiedOn = info.LastWriteTime,
CreatedOn = info.CreationTime,
Size = info.Length,
Parent = Path
};
if (EntryVisible.Invoke(file))
Entries.Add(file);
}
catch { }
}
}
}
async Task UpdateArchiveEntries()
{
var entries = await ArchiveService.GetContents(ArchiveId);
var separator = '/';
foreach (var entry in entries.Where(e => e.FullName != Path.Path && e.FullName.StartsWith(Path.Path) && !e.FullName.Substring(Path.Path.Length).TrimEnd(separator).Contains(separator)))
{
if (entry.FullName.EndsWith(separator))
{
var directory = new FileManagerDirectory
{
Path = entry.FullName,
Name = entry.Name,
ModifiedOn = entry.LastWriteTime.UtcDateTime.ToLocalTime(),
CreatedOn = entry.LastWriteTime.UtcDateTime.ToLocalTime(),
Size = entry.Length,
Parent = Path
};
if (EntryVisible.Invoke(directory))
Entries.Add(directory);
}
else
{
var file = new FileManagerFile
{
Path = entry.FullName,
Name = entry.Name,
ModifiedOn = entry.LastWriteTime.UtcDateTime.ToLocalTime(),
CreatedOn = entry.LastWriteTime.UtcDateTime.ToLocalTime(),
Size = entry.Length,
Parent = Path
};
if (EntryVisible.Invoke(file))
Entries.Add(file);
}
}
}
void UpdateBreadcrumbs()
{
Breadcrumbs.Clear();
var currentPath = Path;
while (currentPath != null)
{
Breadcrumbs.Add(currentPath);
currentPath = currentPath.Parent;
}
Breadcrumbs.Reverse();
}
async Task NavigateBack()
{
if (Past.Count > 0)
{
Future.Add(Path);
await ChangeDirectory(Past.Last(), false);
Past = Past.Take(Past.Count - 1).ToList();
}
}
async Task NavigateForward()
{
if (Future.Count > 0)
{
Past.Add(Path);
await ChangeDirectory(Future.First(), false);
Future = Future.Skip(1).ToList();
}
}
async Task NavigateUp()
{
if (Path.Parent != null)
await ChangeDirectory(Path.Parent, true);
}
async Task Refresh()
{
await ChangeDirectory(Path, false);
StateHasChanged();
}
async Task AddFolder(string name)
{
if (Source == FileManagerSource.Archive)
throw new NotImplementedException();
try
{
Directory.CreateDirectory(System.IO.Path.Combine(Path.Path, name));
await Refresh();
await MessageService.Success("Folder created!");
}
catch
{
await MessageService.Error("Error creating folder!");
}
}
async Task Delete()
{
if (Source == FileManagerSource.Archive)
throw new NotImplementedException();
try
{
foreach (var entry in Selected)
{
if (entry is FileManagerDirectory)
Directory.Delete(entry.Path);
else if (entry is FileManagerFile)
File.Delete(entry.Path);
}
Selected = new List<IFileManagerEntry>();
MessageService.Success("Deleted!");
}
catch
{
MessageService.Error("Error deleting!");
}
await Refresh();
}
}

View File

@ -0,0 +1,56 @@
using System.IO.Compression;
namespace LANCommander.Components.FileManagerComponents
{
public class FileManagerDirectory : FileManagerEntry
{
public bool IsExpanded { get; set; } = false;
public bool HasChildren => Children != null && Children.Count > 0;
public HashSet<FileManagerDirectory> Children { get; set; } = new HashSet<FileManagerDirectory>();
public void PopulateChildren(IEnumerable<ZipArchiveEntry> entries)
{
var path = Path == "/" ? "" : Path;
var childPaths = entries.Where(e => e.FullName.EndsWith('/'));
var directChildren = childPaths.Where(p => p.FullName != path && p.FullName.StartsWith(path) && p.FullName.Substring(path.Length).TrimEnd('/').Split('/').Length == 1);
foreach (var directChild in directChildren)
{
var child = new FileManagerDirectory()
{
Path = directChild.FullName,
Name = directChild.FullName.Substring(path.Length).TrimEnd('/'),
Parent = this
};
child.PopulateChildren(entries);
Children.Add(child);
}
}
public void PopulateChildren(IEnumerable<string> entries)
{
var separator = System.IO.Path.DirectorySeparatorChar;
var childPaths = entries.Where(e => e.StartsWith(Path));
var directChildren = childPaths.Where(p => p != Path && p.Substring(Path.Length + 1).Split(separator).Length == 1);
foreach (var directChild in directChildren)
{
if (!Children.Any(c => c.Path == directChild))
{
var child = new FileManagerDirectory()
{
Path = directChild,
Name = directChild.Substring(Path.Length).TrimStart(separator),
Parent = this
};
child.PopulateChildren(entries);
Children.Add(child);
}
}
}
}
}

View File

@ -0,0 +1,12 @@
namespace LANCommander.Components.FileManagerComponents
{
public abstract class FileManagerEntry : IFileManagerEntry
{
public string Name { get; set; }
public string Path { get; set; }
public long Size { get; set; }
public FileManagerDirectory Parent { get; set; }
public DateTime ModifiedOn { get; set; }
public DateTime CreatedOn { get; set; }
}
}

View File

@ -0,0 +1,15 @@
namespace LANCommander.Components.FileManagerComponents
{
[Flags]
public enum FileManagerFeatures
{
NavigationBack = 0,
NavigationForward = 1,
UpALevel = 2,
Refresh = 4,
Breadcrumbs = 8,
NewFolder = 16,
UploadFile = 32,
Delete = 64,
}
}

View File

@ -0,0 +1,76 @@
namespace LANCommander.Components.FileManagerComponents
{
public class FileManagerFile : FileManagerEntry
{
public string Extension => Name.Contains('.') ? Name.Split('.').Last() : Name;
public string GetIcon()
{
switch (Extension)
{
case "":
return "folder";
case "exe":
return "code";
case "zip":
case "rar":
case "7z":
case "gz":
case "tar":
return "file-zip";
case "wad":
case "pk3":
case "pak":
case "cab":
return "file-zip";
case "txt":
case "cfg":
case "config":
case "ini":
case "yml":
case "yaml":
case "log":
case "doc":
case "nfo":
return "file-text";
case "bat":
case "ps1":
case "json":
return "code";
case "bik":
case "avi":
case "mov":
case "mp4":
case "m4v":
case "mkv":
case "wmv":
case "mpg":
case "mpeg":
case "flv":
return "video-camera";
case "dll":
return "api";
case "hlp":
return "file-unknown";
case "png":
case "bmp":
case "jpeg":
case "jpg":
case "gif":
return "file-image";
default:
return "file";
}
}
}
}

View File

@ -0,0 +1,8 @@
namespace LANCommander.Components.FileManagerComponents
{
public enum FileManagerSource
{
FileSystem,
Archive
}
}

View File

@ -0,0 +1,12 @@
namespace LANCommander.Components.FileManagerComponents
{
public interface IFileManagerEntry
{
public string Name { get; set; }
public string Path { get; set; }
public long Size { get; set; }
public FileManagerDirectory Parent { get; set; }
public DateTime ModifiedOn { get; set; }
public DateTime CreatedOn { get; set; }
}
}

View File

@ -0,0 +1,34 @@
<Modal Title="New Folder" Visible="@Visible" Draggable="true" DragInViewport="false" OnOk="OnOk" OnCancel="Close">
<Input @bind-Value="@Name" />
</Modal>
@code {
[Parameter] public EventCallback<string> OnFolderNameEntered { get; set; }
bool Visible { get; set; } = false;
string Name { get; set; }
protected override async Task OnInitializedAsync()
{
Name = "";
}
public void Open()
{
Name = "";
Visible = true;
}
public void Close()
{
Visible = false;
}
async Task OnOk(MouseEventArgs e)
{
if (OnFolderNameEntered.HasDelegate)
await OnFolderNameEntered.InvokeAsync(Name);
Close();
}
}

View File

@ -0,0 +1,42 @@
<Modal Title="Upload Files" Visible="@Visible" Draggable="true" DragInViewport="false" OnCancel="Close">
<Upload Action="/Upload/File" Name="file" Drag Multiple Data="Data" OnCompleted="OnCompleted">
<p class="ant-upload-drag-icon">
<Icon Type="@IconType.Outline.Upload" />
</p>
<p class="ant-upload-text">Click or Drag Files</p>
</Upload>
</Modal>
@code {
[Parameter] public string Path { get; set; }
[Parameter] public EventCallback OnUploadCompleted { get; set; }
bool Visible = false;
Dictionary<string, object> Data = new Dictionary<string, object>();
protected override void OnParametersSet()
{
Data["Path"] = Path;
}
public void Open()
{
Visible = true;
StateHasChanged();
}
public void Close()
{
Visible = false;
StateHasChanged();
}
async Task OnCompleted()
{
Close();
if (OnUploadCompleted.HasDelegate)
await OnUploadCompleted.InvokeAsync();
}
}

View File

@ -0,0 +1,85 @@
@using LANCommander.Components.FileManagerComponents;
@using LANCommander.Models;
@using System.IO.Compression;
@inject ModalService ModalService
@inject ArchiveService ArchiveService
<Space Style="display: flex">
<SpaceItem Style="flex-grow: 1">
<Input Type="text" @bind-Value="Value" OnChange="ValueChanged" />
</SpaceItem>
@if (ArchiveId != Guid.Empty) {
<SpaceItem>
<Button OnClick="BrowseForFile" Type="@ButtonType.Primary" Icon="@IconType.Outline.FolderOpen" Disabled="!ArchiveExists" />
</SpaceItem>
}
else if (!String.IsNullOrWhiteSpace(Root))
{
<SpaceItem>
<Button OnClick="BrowseForFile" Type="@ButtonType.Primary" Icon="@IconType.Outline.FolderOpen" />
</SpaceItem>
}
</Space>
@code {
[Parameter] public string Value { get; set; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] public EventCallback<string> OnSelected { get; set; }
[Parameter] public Guid ArchiveId { get; set; }
[Parameter] public string Title { get; set; } = "Choose File";
[Parameter] public string OkText { get; set; } = "Select File";
[Parameter] public bool AllowDirectories { get; set; } = false;
[Parameter] public string Prefix { get; set; }
[Parameter] public string Root { get; set; }
[Parameter] public Func<IFileManagerEntry, bool> EntrySelectable { get; set; } = _ => true;
[Parameter] public Func<IFileManagerEntry, bool> EntryVisible { get; set; } = _ => true;
bool ArchiveExists { get; set; } = false;
protected override async Task OnInitializedAsync()
{
if (ArchiveId != Guid.Empty)
ArchiveExists = await ArchiveService.Exists(ArchiveId);
}
private async void BrowseForFile()
{
var modalOptions = new ModalOptions()
{
Title = Title,
Maximizable = false,
DefaultMaximized = true,
Closable = true,
OkText = OkText,
WrapClassName = "file-picker-dialog"
};
var browserOptions = new FilePickerOptions()
{
ArchiveId = ArchiveId,
Root = Root,
Select = true,
Multiple = false,
EntrySelectable = EntrySelectable,
EntryVisible = EntryVisible
};
var modalRef = await ModalService.CreateModalAsync<FilePickerDialog, FilePickerOptions, IEnumerable<IFileManagerEntry>>(modalOptions, browserOptions);
modalRef.OnOk = async (results) =>
{
if (!String.IsNullOrWhiteSpace(Prefix))
Value = Prefix + results?.FirstOrDefault()?.Path;
else
Value = results?.FirstOrDefault()?.Path;
if (ValueChanged.HasDelegate)
await ValueChanged.InvokeAsync(Value);
if (OnSelected.HasDelegate)
await OnSelected.InvokeAsync(Value);
StateHasChanged();
};
}
}

View File

@ -0,0 +1,16 @@
@inherits FeedbackComponent<FilePickerOptions, IEnumerable<IFileManagerEntry>>
@using System.IO.Compression;
@using LANCommander.Components.FileManagerComponents;
@using LANCommander.Models;
<FileManager ArchiveId="@Options.ArchiveId" WorkingDirectory="@Options.Root" @bind-Selected="SelectedFiles" EntrySelectable="Options.EntrySelectable" EntryVisible="Options.EntryVisible" SelectMultiple="Options.Multiple" Features="@(FileManagerFeatures.NavigationBack | FileManagerFeatures.NavigationForward | FileManagerFeatures.UpALevel | FileManagerFeatures.Breadcrumbs)" />
@code {
private IEnumerable<IFileManagerEntry> SelectedFiles { get; set; }
public override async Task OnFeedbackOkAsync(ModalClosingEventArgs args)
{
await base.OkCancelRefWithResult!.OnOk(SelectedFiles);
}
}

View File

@ -0,0 +1,27 @@
<div class="image-picker">
<div class="image-picker-images">
@foreach (var image in Images)
{
<div class="image-picker-image" style="width: @(Size)px; max-height: @(Size)px">
<input type="radio" id="image-picker-image-@image.Key" checked="@(Value == image.Key)" name="SelectedResult" @onchange="@(() => SelectionChanged(image.Key))" />
<label for="image-picker-image-@image.Key"></label>
<img src="@image.Value" />
</div>
}
</div>
</div>
@code {
[Parameter] public double Size { get; set; }
[Parameter] public string Value { get; set; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] public Dictionary<string, string> Images { get; set; }
async Task SelectionChanged(string key)
{
Value = key;
if (ValueChanged.HasDelegate)
await ValueChanged.InvokeAsync(key);
}
}

View File

@ -1,63 +0,0 @@
@using LANCommander.Models;
@using System.IO.Compression;
@inject ModalService ModalService
@inject ArchiveService ArchiveService
<Space Style="display: flex">
<SpaceItem Style="flex-grow: 1">
<Input Type="text" @bind-Value="Value" OnChange="ValueChanged" />
</SpaceItem>
@if (ArchiveId != Guid.Empty) {
<SpaceItem>
<Button OnClick="() => BrowseForFile()" Type="@ButtonType.Primary" Icon="@IconType.Outline.FolderOpen" Disabled="!ArchiveExists" />
</SpaceItem>
}
</Space>
@code {
[Parameter] public string Value { get; set; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] public Guid ArchiveId { get; set; }
[Parameter] public string ArchiveBrowserTitle { get; set; } = "Choose File";
[Parameter] public bool AllowDirectories { get; set; } = false;
bool ArchiveExists { get; set; } = false;
protected override async Task OnInitializedAsync()
{
if (ArchiveId != Guid.Empty)
ArchiveExists = await ArchiveService.Exists(ArchiveId);
}
private async void BrowseForFile()
{
var modalOptions = new ModalOptions()
{
Title = ArchiveBrowserTitle,
Maximizable = false,
DefaultMaximized = true,
Closable = true,
OkText = "Select File"
};
var browserOptions = new ArchiveBrowserOptions()
{
ArchiveId = ArchiveId,
Select = true,
Multiple = false,
AllowDirectories = AllowDirectories
};
var modalRef = await ModalService.CreateModalAsync<ArchiveBrowserDialog, ArchiveBrowserOptions, IEnumerable<ZipArchiveEntry>>(modalOptions, browserOptions);
modalRef.OnOk = async (results) =>
{
Value = "{InstallDir}/" + results.FirstOrDefault().FullName;
if (ValueChanged.HasDelegate)
await ValueChanged.InvokeAsync(Value);
StateHasChanged();
};
}
}

View File

@ -0,0 +1,63 @@
@inherits FeedbackComponent<MediaGrabberOptions, MediaGrabberResult>
@using LANCommander.Data.Enums;
@using LANCommander.Models;
@inject IMediaGrabberService MediaGrabberService
<GridRow Justify="space-between">
<GridCol Span="6">
<Search @bind-Value="Search" OnSearch="(x) => GetResults(Type, x)" DefaultValue="@Search" />
</GridCol>
<GridCol Span="12"></GridCol>
<GridCol Span="6">
<Slider TValue="double" @bind-Value="Size" DefaultValue="200" Min="50" Max="400" />
</GridCol>
</GridRow>
@foreach (var group in Results)
{
<div class="media-grabber-group">
<h2>@group.First().Group</h2>
<ImagePicker Size="Size" Images="@group.ToDictionary(r => r.Id, r => r.ThumbnailUrl)" ValueChanged="OnImageSelected" />
</div>
}
@code {
[Parameter] public string Search { get; set; }
[Parameter] public MediaType Type { get; set; }
MediaGrabberResult Media { get; set; }
double Size { get; set; } = 200;
IEnumerable<IEnumerable<MediaGrabberResult>> Results = new List<List<MediaGrabberResult>>();
Dictionary<string, string> Images { get; set; } = new Dictionary<string, string>();
protected override async Task OnFirstAfterRenderAsync()
{
Type = Options.Type;
Search = Options.Search;
await GetResults(Type, Search);
}
private async Task GetResults(MediaType type, string search)
{
if (!String.IsNullOrWhiteSpace(search))
{
Results = (await MediaGrabberService.SearchAsync(type, search)).GroupBy(r => r.Group);
StateHasChanged();
}
}
private void OnImageSelected(string key)
{
Media = Results.SelectMany(g => g).FirstOrDefault(r => r.Id == key);
}
public override async Task OnFeedbackOkAsync(ModalClosingEventArgs args)
{
await base.OkCancelRefWithResult!.OnOk(Media);
}
}

View File

@ -0,0 +1,86 @@
@inject IMessageService MessageService
<Modal Visible="Visible" OkText="@("Insert")" OnOk="Parse" OnCancel="Close" Width="800" Title="Paste Export File Contents">
<StandaloneCodeEditor @ref="Editor" ConstructionOptions="EditorConstructionOptions" />
</Modal>
@code {
[Parameter] public EventCallback<string> OnParsed { get; set; }
bool Visible = false;
string Contents = "";
StandaloneCodeEditor? Editor;
private StandaloneEditorConstructionOptions EditorConstructionOptions(StandaloneCodeEditor editor)
{
return new StandaloneEditorConstructionOptions
{
AutomaticLayout = true,
Language = "ini",
Value = Contents,
Theme = "vs-dark",
};
}
private async Task Parse()
{
Contents = await Editor.GetValue();
var parser = new RegParserDotNet.RegParser();
var lines = new List<string>();
try
{
var keys = parser.Parse(Contents);
foreach (var key in keys)
{
switch (key.Type)
{
case RegParserDotNet.RegistryValueType.REG_KEY:
if (lines.Count > 0)
lines.Add("");
lines.Add($"New-Item -Path \"registry::\\{key.Path}\"");
break;
case RegParserDotNet.RegistryValueType.REG_SZ:
lines.Add($"New-ItemProperty -Path \"registry::\\{key.Path}\" -Name \"{key.Property}\" -Value \"{(string)key.Value}\" -Force");
break;
case RegParserDotNet.RegistryValueType.REG_DWORD:
lines.Add($"New-ItemProperty -Path \"registry::\\{key.Path}\" -Name \"{key.Property}\" -Value {(int)key.Value} -Force");
break;
case RegParserDotNet.RegistryValueType.REG_BINARY:
var bytes = key.Value as byte[];
var convertedBytes = String.Join("\\\n", bytes.Chunk(32).Select(c => String.Join(", ", c.Select(b => "0x" + b.ToString("X2")))));
lines.Add($"New-ItemProperty -Path \"registry::\\{key.Path}\" -Name \"{key.Property}\" -PropertyType Binary -Value [byte[]]({convertedBytes}) -Force");
break;
}
}
if (OnParsed.HasDelegate)
await OnParsed.InvokeAsync(String.Join('\n', lines));
Close();
StateHasChanged();
}
catch (Exception ex)
{
MessageService.Error(ex.Message);
}
}
public void Open()
{
Visible = true;
}
public void Close()
{
Visible = false;
}
}

View File

@ -0,0 +1,110 @@
@using LANCommander.Components.FileManagerComponents;
@using LANCommander.Data.Enums;
@using LANCommander.Extensions;
@using LANCommander.Models
@using LANCommander.Services
@using System.IO.Compression;
@using Microsoft.EntityFrameworkCore;
@inject ScriptService ScriptService
@inject ModalService ModalService
@inject IMessageService MessageService
<Space Direction="DirectionVHType.Vertical" Size="@("large")" Style="width: 100%">
<SpaceItem>
<Table TItem="Script" DataSource="@Scripts" HidePagination="true" Responsive>
<PropertyColumn Property="s => s.Type">@context.Type.GetDisplayName()</PropertyColumn>
<PropertyColumn Property="s => s.CreatedBy">
@context.CreatedBy?.UserName
</PropertyColumn>
<PropertyColumn Property="s => s.CreatedOn" Format="MM/dd/yyyy hh:mm tt" />
<ActionColumn Title="">
<Space Style="display: flex; justify-content: end">
<SpaceItem>
<Button OnClick="() => Edit(context.Id)" Icon="@IconType.Outline.Edit" Type="@ButtonType.Text" />
</SpaceItem>
<SpaceItem>
<Popconfirm OnConfirm="() => Delete(context)" Title="Are you sure you want to delete this script?">
<Button Icon="@IconType.Outline.Close" Type="@ButtonType.Text" Danger />
</Popconfirm>
</SpaceItem>
</Space>
</ActionColumn>
</Table>
</SpaceItem>
<SpaceItem>
<GridRow Justify="end">
<GridCol>
<Button OnClick="() => Edit()" Type="@ButtonType.Primary">Add Script</Button>
</GridCol>
</GridRow>
</SpaceItem>
</Space>
<style>
.monaco-editor-container {
height: 600px;
}
</style>
@code {
[Parameter] public Guid GameId { get; set; }
[Parameter] public Guid RedistributableId { get; set; }
[Parameter] public Guid ArchiveId { get; set; }
[Parameter] public IEnumerable<ScriptType> AllowedTypes { get; set; }
ICollection<Script> Scripts { get; set; } = new List<Script>();
protected override async Task OnParametersSetAsync()
{
await LoadData();
}
private async Task LoadData()
{
if (GameId != Guid.Empty)
Scripts = await ScriptService.Get(s => s.GameId == GameId).ToListAsync();
else if (RedistributableId != Guid.Empty)
Scripts = await ScriptService.Get(s => s.RedistributableId == RedistributableId).ToListAsync();
await InvokeAsync(StateHasChanged);
}
private async void Edit(Guid? scriptId = null)
{
var modalOptions = new ModalOptions()
{
Title = scriptId == null ? "Add Script" : "Edit Script",
Maximizable = false,
DefaultMaximized = true,
Closable = true,
OkText = "Save"
};
var options = new ScriptEditorOptions()
{
ScriptId = scriptId ?? default,
AllowedTypes = AllowedTypes,
ArchiveId = ArchiveId,
GameId = GameId,
RedistributableId = RedistributableId
};
var modalRef = await ModalService.CreateModalAsync<ScriptEditorDialog, ScriptEditorOptions, Script>(modalOptions, options);
modalRef.OnOk = async (script) =>
{
await LoadData();
};
}
private async void Delete(Script script = null)
{
if (script != null)
await ScriptService.Delete(script);
await MessageService.Success("Script deleted!");
}
}

View File

@ -0,0 +1,195 @@
@using LANCommander.Components.FileManagerComponents;
@using LANCommander.Extensions;
@using LANCommander.Data.Enums;
@using LANCommander.Models;
@inherits FeedbackComponent<ScriptEditorOptions, Script>
@inject ScriptService ScriptService
@inject ModalService ModalService
@inject IMessageService MessageService
<Form Model="@Script" Layout="@FormLayout.Vertical">
<FormItem>
@foreach (var group in Snippets.Select(s => s.Group).Distinct())
{
<Dropdown>
<Overlay>
<Menu>
@foreach (var snippet in Snippets.Where(s => s.Group == group))
{
<MenuItem OnClick="() => InsertSnippet(snippet)">
@snippet.Name
</MenuItem>
}
</Menu>
</Overlay>
<ChildContent>
<Button Type="@ButtonType.Primary">@group</Button>
</ChildContent>
</Dropdown>
}
@if (Options.ArchiveId != Guid.Empty)
{
<Button Icon="@IconType.Outline.FolderOpen" OnClick="BrowseForPath" Type="@ButtonType.Text">Browse</Button>
}
<Button Icon="@IconType.Outline.Build" OnClick="() => RegToPowerShell.Open()" Type="@ButtonType.Text">Import .reg</Button>
</FormItem>
<FormItem>
<StandaloneCodeEditor @ref="Editor" Id="@("editor-" + Id.ToString())" ConstructionOptions="EditorConstructionOptions" />
</FormItem>
<FormItem Label="Name">
<Input @bind-Value="@context.Name" />
</FormItem>
<FormItem Label="Type">
<Select @bind-Value="context.Type" TItem="ScriptType" TItemValue="ScriptType" DataSource="Enum.GetValues<ScriptType>().Where(st => Options.AllowedTypes == null || Options.AllowedTypes.Contains(st))">
<LabelTemplate Context="Value">@Value.GetDisplayName()</LabelTemplate>
<ItemTemplate Context="Value">@Value.GetDisplayName()</ItemTemplate>
</Select>
</FormItem>
<FormItem>
<Checkbox @bind-Checked="context.RequiresAdmin">Requires Admin</Checkbox>
</FormItem>
<FormItem Label="Description">
<TextArea @bind-Value="context.Description" MaxLength=500 ShowCount />
</FormItem>
</Form>
<RegToPowerShell @ref="RegToPowerShell" OnParsed="(text) => InsertText(text)" />
@code {
Guid Id = Guid.NewGuid();
Script Script;
StandaloneCodeEditor? Editor;
RegToPowerShell RegToPowerShell;
IEnumerable<Snippet> Snippets { get; set; }
private StandaloneEditorConstructionOptions EditorConstructionOptions(StandaloneCodeEditor editor)
{
return new StandaloneEditorConstructionOptions
{
AutomaticLayout = true,
Language = "powershell",
Value = Script.Contents,
Theme = "vs-dark",
};
}
protected override async Task OnInitializedAsync()
{
if (Options.ScriptId != Guid.Empty)
Script = await ScriptService.Get(Options.ScriptId);
else if (Options.GameId != Guid.Empty)
Script = new Script()
{
GameId = Options.GameId
};
else if (Options.RedistributableId != Guid.Empty)
Script = new Script()
{
RedistributableId = Options.RedistributableId
};
Snippets = ScriptService.GetSnippets();
}
public override async Task OnFeedbackOkAsync(ModalClosingEventArgs args)
{
await Save();
Editor.Dispose();
await base.OkCancelRefWithResult!.OnOk(Script);
}
public override async Task CancelAsync(ModalClosingEventArgs args)
{
Editor.Dispose();
await base.CancelAsync(args);
}
private async void BrowseForPath()
{
var modalOptions = new ModalOptions()
{
Title = "Choose Reference",
Maximizable = false,
DefaultMaximized = true,
Closable = true,
OkText = "Insert File Path"
};
var browserOptions = new FilePickerOptions()
{
ArchiveId = Options.ArchiveId,
Select = true,
Multiple = false
};
var modalRef = await ModalService.CreateModalAsync<FilePickerDialog, FilePickerOptions, IEnumerable<IFileManagerEntry>>(modalOptions, browserOptions);
modalRef.OnOk = (results) =>
{
var path = results.FirstOrDefault().Path;
InsertText($"$InstallDir\\{path.Replace('/', '\\')}");
StateHasChanged();
return Task.CompletedTask;
};
}
private async Task InsertText(string text)
{
var line = await Editor.GetPosition();
var range = new BlazorMonaco.Range(line.LineNumber, 1, line.LineNumber, 1);
var currentSelections = await Editor.GetSelections();
await Editor.ExecuteEdits("ScriptEditor", new List<IdentifiedSingleEditOperation>()
{
new IdentifiedSingleEditOperation
{
Range = range,
Text = text,
ForceMoveMarkers = true
}
}, currentSelections);
}
private async Task InsertSnippet(Snippet snippet)
{
await InsertText(snippet.Content);
}
private async Task Save()
{
try
{
var value = await Editor.GetValue();
Script.Contents = value;
if (Script.Id == Guid.Empty)
Script = await ScriptService.Add(Script);
else
Script = await ScriptService.Update(Script);
MessageService.Success("Script saved!");
}
catch (Exception ex)
{
MessageService.Error("Script could not be saved!");
throw ex;
}
}
}

View File

@ -2,7 +2,7 @@
<Select Mode="tags" TItem="Guid" TItemValue="Guid" @bind-Values="@SelectedValues" OnSelectedItemsChanged="OnSelectedItemsChanged" EnableSearch>
<SelectOptions>
@foreach (var entity in Entities)
@foreach (var entity in Entities.OrderBy(OptionLabelSelector))
{
<SelectOption TItemValue="Guid" TItem="Guid" Value="@entity.Id" Label="@OptionLabelSelector.Invoke(entity)" />
}
@ -45,7 +45,7 @@
}
if (ValuesChanged.HasDelegate)
await ValuesChanged.InvokeAsync();
await ValuesChanged.InvokeAsync(Values);
StateHasChanged();
}

View File

@ -0,0 +1,38 @@
@typeparam TItem where TItem : BaseModel
<Transfer DataSource="TransferItems" TargetKeys="TargetKeys" OnChange="OnChange" Titles="new string[] { LeftTitle, RightTitle }" />
@code {
[Parameter] public string LeftTitle { get; set; } = "";
[Parameter] public string RightTitle { get; set; } = "";
[Parameter] public Func<TItem, string> TitleSelector { get; set; }
[Parameter] public IEnumerable<TItem> DataSource { get; set; }
[Parameter] public ICollection<TItem> Values { get; set; } = new List<TItem>();
[Parameter] public EventCallback<ICollection<TItem>> ValuesChanged { get; set; }
IEnumerable<TransferItem> TransferItems { get; set; } = new List<TransferItem>();
List<string> TargetKeys { get; set; } = new List<string>();
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
TransferItems = DataSource.Select(i => new TransferItem()
{
Key = i.Id.ToString(),
Title = TitleSelector.Invoke(i)
});
if (Values != null)
TargetKeys = Values.Select(i => i.Id.ToString()).ToList();
}
}
async Task OnChange(TransferChangeArgs e)
{
Values = DataSource.Where(i => e.TargetKeys.Contains(i.Id.ToString())).ToList();
if (ValuesChanged.HasDelegate)
await ValuesChanged.InvokeAsync(Values);
}
}

View File

@ -1,6 +1,8 @@
using LANCommander.Data;
using LANCommander.Data.Models;
using LANCommander.Extensions;
using LANCommander.Models;
using LANCommander.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -12,7 +14,8 @@ namespace LANCommander.Controllers.Api
[ApiController]
public class ArchivesController : ControllerBase
{
private DatabaseContext Context;
private readonly DatabaseContext Context;
private readonly LANCommanderSettings Settings = SettingService.GetSettings();
public ArchivesController(DatabaseContext context)
{
@ -47,7 +50,7 @@ namespace LANCommander.Controllers.Api
if (archive == null)
return NotFound();
var filename = Path.Combine("Upload", archive.ObjectKey);
var filename = Path.Combine(Settings.Archives.StoragePath, archive.ObjectKey);
if (!System.IO.File.Exists(filename))
return NotFound();

View File

@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using NLog;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
@ -35,6 +36,8 @@ namespace LANCommander.Controllers.Api
[ApiController]
public class AuthController : ControllerBase
{
protected readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly UserManager<User> UserManager;
private readonly IUserStore<User> UserStore;
private readonly RoleManager<Role> RoleManager;
@ -57,10 +60,14 @@ namespace LANCommander.Controllers.Api
{
var token = await Login(user, model.Password);
Logger.Debug("Successfully logged in user {UserName}", user.UserName);
return Ok(token);
}
catch
catch (Exception ex)
{
Logger.Error(ex, "An error occurred while trying to log in {UserName}", model.UserName);
return Unauthorized();
}
}
@ -80,6 +87,7 @@ namespace LANCommander.Controllers.Api
{
if (token == null)
{
Logger.Debug("Null token passed when trying to refresh");
return BadRequest("Invalid client request");
}
@ -87,6 +95,7 @@ namespace LANCommander.Controllers.Api
if (principal == null)
{
Logger.Debug("Invalid access token or refresh token");
return BadRequest("Invalid access token or refresh token");
}
@ -94,6 +103,7 @@ namespace LANCommander.Controllers.Api
if (user == null || user.RefreshToken != token.RefreshToken || user.RefreshTokenExpiration <= DateTime.Now)
{
Logger.Debug("Invalid access token or refresh token for user {UserName}", principal.Identity.Name);
return BadRequest("Invalid access token or refresh token");
}
@ -104,6 +114,8 @@ namespace LANCommander.Controllers.Api
await UserManager.UpdateAsync(user);
Logger.Debug("Successfully refreshed token for user {UserName}", user.UserName);
return Ok(new
{
AccessToken = new JwtSecurityTokenHandler().WriteToken(newAccessToken),
@ -118,10 +130,14 @@ namespace LANCommander.Controllers.Api
var user = await UserManager.FindByNameAsync(model.UserName);
if (user != null)
{
Logger.Debug("Cannot register user with username {UserName}, already exists", model.UserName);
return Unauthorized(new
{
Message = "Username is unavailable"
});
}
user = new User();
@ -135,10 +151,13 @@ namespace LANCommander.Controllers.Api
{
var token = await Login(user, model.Password);
Logger.Debug("Successfully registered user {UserName}", user.UserName);
return Ok(token);
}
catch
catch (Exception ex)
{
Logger.Error(ex, "Could not register user {UserName}", user.UserName);
return BadRequest(new
{
Message = "An unknown error occurred"
@ -156,6 +175,8 @@ namespace LANCommander.Controllers.Api
{
if (user != null && await UserManager.CheckPasswordAsync(user, password))
{
Logger.Debug("Password check for user {UserName} was successful", user.UserName);
if (Settings.Authentication.RequireApproval && !user.Approved)
throw new Exception("Account must be approved by an administrator");
@ -172,6 +193,8 @@ namespace LANCommander.Controllers.Api
authClaims.Add(new Claim(ClaimTypes.Role, userRole));
}
Logger.Debug("Generating authentication token for user {UserName}", user.UserName);
var token = GetToken(authClaims);
var refreshToken = GenerateRefreshToken();

View File

@ -18,12 +18,14 @@ namespace LANCommander.Controllers.Api
private readonly GameSaveService GameSaveService;
private readonly GameService GameService;
private readonly UserManager<User> UserManager;
private readonly LANCommanderSettings Settings;
public GameSavesController(GameSaveService gameSaveService, GameService gameService, UserManager<User> userManager)
{
GameSaveService = gameSaveService;
GameService = gameService;
UserManager = userManager;
Settings = SettingService.GetSettings();
}
[HttpGet("{id}")]
@ -65,7 +67,7 @@ namespace LANCommander.Controllers.Api
public async Task<IActionResult> Upload(Guid id, [FromForm] SaveUpload save)
{
// Arbitrary file size limit of 25MB
if (save.File.Length > (ByteSizeLib.ByteSize.BytesInMebiByte * 25))
if (save.File.Length > (ByteSizeLib.ByteSize.BytesInMebiByte * Settings.UserSaves.MaxSize))
return BadRequest("Save file archive is too large");
var game = await GameService.Get(id);

View File

@ -15,6 +15,7 @@ namespace LANCommander.Controllers.Api
public class GamesController : ControllerBase
{
private readonly GameService GameService;
private readonly LANCommanderSettings Settings = SettingService.GetSettings();
public GamesController(GameService gameService)
{
@ -41,8 +42,6 @@ namespace LANCommander.Controllers.Api
{
var manifest = await GameService.GetManifest(id);
manifest.Icon = Url.Action(nameof(GetIcon), new { id = id });
return manifest;
}
@ -59,28 +58,12 @@ namespace LANCommander.Controllers.Api
var archive = game.Archives.OrderByDescending(a => a.CreatedOn).First();
var filename = Path.Combine("Upload", archive.ObjectKey);
var filename = Path.Combine(Settings.Archives.StoragePath, archive.ObjectKey);
if (!System.IO.File.Exists(filename))
return NotFound();
return File(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read), "application/octet-stream", $"{game.Title.SanitizeFilename()}.zip");
}
[AllowAnonymous]
[HttpGet("{id}/Icon.png")]
public async Task<IActionResult> GetIcon(Guid id)
{
try
{
var game = await GameService.Get(id);
return File(GameService.GetIcon(game), "image/png");
}
catch (FileNotFoundException ex)
{
return NotFound();
}
}
}
}

View File

@ -0,0 +1,56 @@
using LANCommander.Data;
using LANCommander.Data.Models;
using LANCommander.Extensions;
using LANCommander.Models;
using LANCommander.SDK;
using LANCommander.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LANCommander.Controllers.Api
{
[Authorize(AuthenticationSchemes = "Bearer")]
[Route("api/[controller]")]
[ApiController]
public class MediaController : ControllerBase
{
private readonly MediaService MediaService;
private readonly LANCommanderSettings Settings = SettingService.GetSettings();
public MediaController(MediaService mediaService)
{
MediaService = mediaService;
}
[HttpGet]
public async Task<IEnumerable<Media>> Get()
{
return await MediaService.Get();
}
[HttpGet("{id}")]
public async Task<Media> Get(Guid id)
{
return await MediaService.Get(id);
}
[AllowAnonymous]
[HttpGet("{id}/Download")]
public async Task<IActionResult> Download(Guid id)
{
try
{
var media = await MediaService.Get(id);
var fs = System.IO.File.OpenRead(MediaService.GetImagePath(media));
return File(fs, media.MimeType);
}
catch (Exception ex)
{
return NotFound();
}
}
}
}

View File

@ -0,0 +1,53 @@
using LANCommander.Data.Models;
using LANCommander.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace LANCommander.Controllers.Api
{
[Authorize(AuthenticationSchemes = "Bearer")]
[Route("api/[controller]")]
[ApiController]
public class PlaySessionsController : ControllerBase
{
private readonly PlaySessionService PlaySessionService;
private readonly GameService GameService;
private readonly UserManager<User> UserManager;
public PlaySessionsController(PlaySessionService playSessionService, GameService gameService, UserManager<User> userManager)
{
PlaySessionService = playSessionService;
GameService = gameService;
UserManager = userManager;
}
[HttpPost("Start/{id}")]
public async Task<IActionResult> Start(Guid id)
{
var user = await UserManager.FindByNameAsync(User.Identity.Name);
var game = await GameService.Get(id);
if (game == null || user == null)
return BadRequest();
await PlaySessionService.StartSession(game.Id, user.Id);
return Ok();
}
[HttpPost("End/{id}")]
public async Task<IActionResult> End(Guid id)
{
var user = await UserManager.FindByNameAsync(User.Identity.Name);
var game = await GameService.Get(id);
if (game == null || user == null)
return BadRequest();
await PlaySessionService.EndSession(game.Id, user.Id);
return Ok();
}
}
}

View File

@ -0,0 +1,60 @@
using LANCommander.Data.Models;
using LANCommander.Models;
using LANCommander.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using NLog;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
namespace LANCommander.Controllers.Api
{
[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = "Bearer")]
[ApiController]
public class ProfileController : ControllerBase
{
protected readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly UserManager<User> UserManager;
public ProfileController(UserManager<User> userManager)
{
UserManager = userManager;
}
[HttpGet]
public async Task<IActionResult> Get()
{
if (User != null && User.Identity != null && User.Identity.IsAuthenticated)
{
var user = await UserManager.FindByNameAsync(User.Identity.Name);
return Ok(user);
}
else
return Unauthorized();
}
[HttpPost]
public async Task<IActionResult> ChangeAlias(string alias)
{
if (User != null && User.Identity != null && User.Identity.IsAuthenticated)
{
var user = await UserManager.FindByNameAsync(User.Identity.Name);
user.Alias = alias;
await UserManager.UpdateAsync(user);
return Ok();
}
else
return Unauthorized();
}
}
}

View File

@ -0,0 +1,57 @@
using LANCommander.Data.Models;
using LANCommander.Extensions;
using LANCommander.Models;
using LANCommander.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LANCommander.Controllers.Api
{
[Authorize(AuthenticationSchemes = "Bearer")]
[Route("api/[controller]")]
[ApiController]
public class RedistributableController : ControllerBase
{
private readonly RedistributableService RedistributableService;
private readonly LANCommanderSettings Settings = SettingService.GetSettings();
public RedistributableController(RedistributableService redistributableService)
{
RedistributableService = redistributableService;
}
[HttpGet]
public async Task<IEnumerable<Redistributable>> Get()
{
return await RedistributableService.Get();
}
[HttpGet("{id}")]
public async Task<Redistributable> Get(Guid id)
{
return await RedistributableService.Get(id);
}
[HttpGet("{id}/Download")]
public async Task<IActionResult> Download(Guid id)
{
var redistributable = await RedistributableService.Get(id);
if (redistributable == null)
return NotFound();
if (redistributable.Archives == null || redistributable.Archives.Count == 0)
return NotFound();
var archive = redistributable.Archives.OrderByDescending(a => a.CreatedOn).First();
var filename = Path.Combine(Settings.Archives.StoragePath, archive.ObjectKey);
if (!System.IO.File.Exists(filename))
return NotFound();
return File(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read), "application/octet-stream", $"{redistributable.Name.SanitizeFilename()}.zip");
}
}
}

View File

@ -1,4 +1,5 @@
using LANCommander.Models;
using LANCommander.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@ -8,18 +9,18 @@ namespace LANCommander.Controllers.Api
[ApiController]
public class UploadController : ControllerBase
{
private const string UploadDirectory = "Upload";
private readonly LANCommanderSettings Settings = SettingService.GetSettings();
[HttpPost("Init")]
public string Init()
{
var key = Guid.NewGuid().ToString();
if (!Directory.Exists(UploadDirectory))
Directory.CreateDirectory(UploadDirectory);
if (!Directory.Exists(Settings.Archives.StoragePath))
Directory.CreateDirectory(Settings.Archives.StoragePath);
if (!System.IO.File.Exists(Path.Combine(UploadDirectory, key)))
System.IO.File.Create(Path.Combine(UploadDirectory, key)).Close();
if (!System.IO.File.Exists(Path.Combine(Settings.Archives.StoragePath, key)))
System.IO.File.Create(Path.Combine(Settings.Archives.StoragePath, key)).Close();
return key;
}
@ -27,7 +28,7 @@ namespace LANCommander.Controllers.Api
[HttpPost("Chunk")]
public async Task Chunk([FromForm] ChunkUpload chunk)
{
var filePath = Path.Combine(UploadDirectory, chunk.Key.ToString());
var filePath = Path.Combine(Settings.Archives.StoragePath, chunk.Key.ToString());
if (!System.IO.File.Exists(filePath))
throw new Exception("Destination file not initialized.");
@ -47,5 +48,11 @@ namespace LANCommander.Controllers.Api
}
}
}
[HttpPost("Media")]
public async Task Media(IFormFile file)
{
}
}
}

View File

@ -12,25 +12,33 @@ namespace LANCommander.Controllers
public class DownloadController : Controller
{
private readonly ArchiveService ArchiveService;
private readonly LANCommanderSettings Settings = SettingService.GetSettings();
public DownloadController(ArchiveService archiveService)
{
ArchiveService = archiveService;
}
public async Task<IActionResult> Game(Guid id)
public async Task<IActionResult> Archive(Guid id)
{
var archive = await ArchiveService.Get(id);
if (archive == null)
return NotFound();
var filename = Path.Combine("Upload", archive.ObjectKey);
var filename = Path.Combine(Settings.Archives.StoragePath, archive.ObjectKey);
if (!System.IO.File.Exists(filename))
return NotFound();
return File(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read), "application/octet-stream", $"{archive.Game.Title.SanitizeFilename()}.zip");
string name = "";
if (archive.GameId != null && archive.GameId != Guid.Empty)
name = $"{archive.Game.Title.SanitizeFilename()}.zip";
else if (archive.RedistributableId != null && archive.RedistributableId != Guid.Empty)
name = $"{archive.Redistributable.Name.SanitizeFilename()}.zip";
return File(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read), "application/octet-stream", name);
}
}
}

View File

@ -0,0 +1,36 @@
using LANCommander.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LANCommander.Controllers
{
[Authorize]
public class SavesController : Controller
{
private readonly GameSaveService GameSaveService;
public SavesController(GameSaveService gameSaveService)
{
GameSaveService = gameSaveService;
}
[HttpGet]
public async Task<IActionResult> Download(Guid id)
{
var save = await GameSaveService.Get(id);
if (User == null || User.Identity?.Name != save.User?.UserName)
return Unauthorized();
if (save == null)
return NotFound();
var filename = GameSaveService.GetSavePath(save);
if (!System.IO.File.Exists(filename))
return NotFound();
return File(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read), "application/zip", $"{save.User?.UserName} - {(save.Game == null ? "Unknown" : save.Game?.Title)} - {save.CreatedOn.ToString("MM-dd-yyyy.hh-mm")}.zip");
}
}
}

View File

@ -0,0 +1,63 @@
using LANCommander.Services;
using Microsoft.AspNetCore.Mvc;
namespace LANCommander.Controllers
{
public class ServerController : Controller
{
private readonly ServerService ServerService;
public ServerController(ServerService serverService)
{
this.ServerService = serverService;
}
[HttpGet("/Server/{id:guid}/{*path}")]
public async Task<IActionResult> Web(Guid id, string path)
{
var server = await ServerService.Get(id);
if (server == null)
return NotFound();
if (server.HttpPaths == null || server.HttpPaths.Count == 0)
return NotFound();
// Sanitize
if (path == null)
path = "/";
path = path.Trim('/');
path = path + "/";
var httpPath = server.HttpPaths.FirstOrDefault(hp => path.StartsWith(hp.Path.TrimStart('/')));
// Check to see if there's a root path defined if nothing else matches
if (httpPath == null)
httpPath = server.HttpPaths.FirstOrDefault(hp => hp.Path == "/");
if (httpPath == null)
return Forbid();
var relativePath = path.Substring(httpPath.Path.TrimStart('/').Length).Replace('/', Path.DirectorySeparatorChar).TrimStart('\\');
var localPath = Path.Combine(httpPath.LocalPath, relativePath).TrimEnd('\\');
var attrs = System.IO.File.GetAttributes(localPath);
if ((attrs & FileAttributes.Directory) == FileAttributes.Directory)
{
if (!System.IO.Directory.Exists(localPath))
return NotFound();
return Json(Directory.GetFileSystemEntries(localPath).Select(fse => fse.Substring(localPath.Length)));
}
else
{
if (!System.IO.File.Exists(localPath))
return NotFound();
return File(new FileStream(localPath, FileMode.Open, FileAccess.Read, FileShare.Read), "application/octet-stream", Path.GetFileName(localPath));
}
}
}
}

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