diff --git a/.github/workflows/build-clifp-windows.yml b/.github/workflows/build-clifp-windows.yml index 105b2ef..57c9375 100644 --- a/.github/workflows/build-clifp-windows.yml +++ b/.github/workflows/build-clifp-windows.yml @@ -67,7 +67,7 @@ jobs: echo "Build complete." - name: Get CLIFp artifact name run: | - $artifact_name=$((Get-ChildItem -Path "${{ env.clifp_package_path }}" -Filter *.zip)[0].BaseName) + $artifact_name=$((Get-ChildItem -Path "${{ env.clifp_package_path }}" -Filter *.zip)[0].BaseName) + ' [msvc]' echo "current_artifact_name=$artifact_name" >> $Env:GITHUB_ENV - name: Upload CLIFp build artifact uses: actions/upload-artifact@v3 diff --git a/.github/workflows/master-pull-request-merge-reaction.yml b/.github/workflows/master-pull-request-merge-reaction.yml index 6bff98e..7ccf682 100644 --- a/.github/workflows/master-pull-request-merge-reaction.yml +++ b/.github/workflows/master-pull-request-merge-reaction.yml @@ -106,7 +106,7 @@ jobs: - name: Zip up release artifacts shell: pwsh run: | - $artifact_folders = Get-ChildItem -Directory -Path "${{ env.artifacts_path }}" + $artifact_folders = Get-ChildItem -Directory -Path "${{ env.artifacts_path }}" -Exclude "github-pages" foreach($art_dir in $artifact_folders) { $name = $art_dir.name diff --git a/CMakeLists.txt b/CMakeLists.txt index 9af0253..c4a23f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.24.0...3.26.0) # Project # NOTE: DON'T USE TRAILING ZEROS IN VERSIONS project(CLIFp - VERSION 0.9.8 + VERSION 0.9.9 LANGUAGES CXX DESCRIPTION "Command-line Interface for Flashpoint Archive" ) @@ -72,14 +72,14 @@ endif() include(OB/FetchQx) ob_fetch_qx( - REF "v0.5.4" + REF "v0.5.5.1" COMPONENTS ${CLIFP_QX_COMPONENTS} ) # Fetch libfp (build and import from source) include(OB/Fetchlibfp) -ob_fetch_libfp("v0.5.1") +ob_fetch_libfp("v0.5.1.1") # Fetch QI-QMP (build and import from source) include(OB/FetchQI-QMP) diff --git a/README.md b/README.md index f969b2c..5171f9b 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,53 @@ # CLIFp (Command-line Interface for Flashpoint) -CLIFp (pronounced "Cliff-P") is a command-line interface for [Flashpoint Archive](https://flashpointarchive.org/) that allows starting games/animations from within the collection via a system's command parser and contextual arguments. While it is a separate application, CLIFp functions closely to that of a native CLI by parsing the configuration of the Flashpoint install it is deployed into and therefore launches games/animation in the same manner as the standard GUI launcher. +CLIFp (pronounced "Cliff-P", or just "Cliff") is an alternative launcher for [Flashpoint Archive](https://flashpointarchive.org/) that focuses on speed and simplicity while providing access to a Flashpoint's vast library via a ripe command-line interface. This greatly increases the flexibility of Flashpoint by allowing for more direct integration with other software, such as Steam, LaunchBox, or any arbitrary application/script. The functionality of CLIFp should be near identical to the standard launcher for the facilities it implements. -Other than a few pop-up dialogs used for alerts and errors, CLIFp runs completely in the background so that the only windows seen during use are the same ones present while running standard Flashpoint. It automatically terminates once the target application has exited, requiring no manual tasks or clean-up by the user. +Other than a few pop-up dialogs used for alerts and errors, CLIFp runs completely in the background so that the only windows seen during use are the same ones present while running standard Flashpoint. It automatically terminates once the target application has exited, requiring no manual tasks or clean-up by the user. [![Dev Builds](https://github.com/oblivioncth/CLIFp/actions/workflows/push-reaction.yml/badge.svg?branch=dev)](https://github.com/oblivioncth/CLIFp/actions/workflows/push-reaction.yml) -## Compatability -### General -Because it directly mimics the actions of the GUI launcher, CLIFp should provide a perfect or near-perfect experience when compared to using the standard method of launching games/animations. Additionally, this makes it fairly resilient towards Flashpoint updates and will most likely only require compatibility patches when updates that make major changes are released. +## Quickstart + +Download the latest **static** [release](https://github.com/oblivioncth/CLIFp/releases) that's appropriate for your system and place it in the root of your Flashpoint directory. + +Play a game: + + # By title + CLIFp play -t "Simple, Tasty Buttons" + + # By UUID + CLIFp play -i 37e5c215-9c39-4a3d-9912-b4343a17027e -All Flashpoint features are supported, other than editing the local database and querying title meta-data through the command-line. More specifically, one can launch: + # Random + CLIFp play -r + +Play an additional app: + + CLIFp play -t "Railroad Rampage" -s "Unlimited Health / Ammo Hack" + +Create a desktop shortcut: + + CLIFp link -t "Skill Archer" + +Create a share link for other users + + # Enable CLIFp to open share links (only needs to be done once) + CLIFp share -c + + # Create link to game + CLIFp share -t "The Ultimate Showdown of Ultimate Destiny" - - Games (with or without autorun-before additional apps) - - Animations - - Additional Apps (including messages) - - Extras - - Anything else I'm forgetting +Update: -While testing for complete compatibility is infeasible given the size of Flashpoint, CLIFp was designed with full compatibility in mind and theoretically is 100% compatible with the Flashpoint collection. Games are slightly prioritized when it comes to testing the application however. + CLIFp update + +## Compatability + +### General +All Flashpoint features are generally supported, other than editing configuration files and user data (like playlists) and querying title meta-data through the command-line, and searching is more limited. See the [All Commands/Options](#all-commandsoptions) section for more information. + +While constantly testing for complete compatibility is infeasible given the size of Flashpoint, CLIFp was designed with full compatibility in mind and theoretically is 100% compatible with the Flashpoint collection. ### Version Matching Each release of this application targets a specific version series of Flashpoint Archive, which are composed of a major and minor version number, and are designed to work with all Flashpoint updates within that series. For example, a FIL release that targets Flashpoint 10.1 is intended to be used with any version of Flashpoint that fits the scheme `10.1.x.x`, such as `10.1`, `10.1.0.3`, `10.1.2`, etc, but **not** `10.2`. @@ -31,37 +59,40 @@ The title of each [release](https://stackedit.io/github.com/oblivioncth/CLIFp/re Updates will always be set to target the latest Flashpoint release, even if they were not created explicitly for compatibility reasons. ## Usage -### Target Usage -CLIFp was primarily created for use with its sister project [FIL (Flashpoint Importer for Launchers)](https://github.com/oblivioncth/FIL) to facilitate the inclusion of Flashpoint in to LaunchBox and other frontend collections. The operation of CLIFp is completely automated when used in this manner. -It was later refined to also be used directly with the Flashpoint project to allow users to create shortcuts and leverage other benefits of a CLI. +### Original Usage +CLIFp was originally created for use with its sister project [FIL (Flashpoint Importer for Launchers)](https://github.com/oblivioncth/FIL) to facilitate the inclusion of Flashpoint in to LaunchBox and other frontend collections. The operation of CLIFp is completely automated when used in this manner. + +It was later refined to also be used directly with the Flashpoint project to allow users to create shortcuts and leverage other benefits of a CLI. That being said, it is perfectly possible to use CLIFp in any manner one sees fit. -It is recommended to place CLIFp in the root directory of Flashpoint (next to its shortcut), but CLIFp will search all parent directories in order for the root Flashpoint structure and therefore will work correctly in any Flashpoint sub-folder. However, this obviously won't work if CLIFp is behind a symlink/junction. +### General -### Builds -**In most cases you should use the 'static' builds of CLIFp on Windows or Linux.** +> [!NOTE] +> In most cases you should use the 'static' builds of CLIFp on Windows or Linux. -### General -**Before using CLIFp, be sure to have ran Flashpoint through its regular launcher at least once. If using Infinity, it's also best to make sure your install is fully updated as well.** +It is recommended to place CLIFp in the root directory of Flashpoint (next to its shortcut), but CLIFp will search all parent directories in order for the root Flashpoint structure and therefore will work correctly in any Flashpoint sub-folder. However, this obviously won't work if CLIFp is behind a symlink/junction. -**NOTE: Do not run CLIFp as an administrator/root as some titles may not work correctly or run at all** +> [!IMPORTANT] +> Before using CLIFp, be sure to have ran Flashpoint through its regular launcher at least once. If using Infinity, it's also best to make sure your install is fully updated as well. +> +> Do not run CLIFp as an administrator/root as some titles may not work correctly or run at all. CLIFp uses the following syntax scheme: CLIFp *command* -The order of switches within each options section does not matter. +The order of switches within each options section does not matter. **Primary Usage:** -The most common use case is the **play** command with the **-i** switch, followed by the UUID of a Flashpoint title: +The most common use case is the **play** command with a [Title Command](#title-commands) switch. For this example **-i** is used to indicate a title is being referenced by its UUID CLIFp play -i 37e5c215-9c39-4a3d-9912-b4343a17027e This is the most straightforward and hassle free approach, as it will start that title exactly as if it had been launched from the Flashpoint GUI (including download/mounting of data packs, autorun before additional apps, etc.). -A title's ID can be found by right clicking on an entry in Flashpoint and selecting "Copy Game UUID". This command also supports starting additional apps, though getting their UUID is more tricky as I currently know of no other way than opening [FP Install Dir]\Data\flashpoint.sqlite in a database browser and searching for the ID manually. I will look into if an easier way to obtain their IDs can be implemented. +A title's ID can be found by right clicking on an entry in Flashpoint and selecting "Copy Game UUID". This command also supports starting additional apps, though getting their UUID is more tricky as I currently know of no other way than opening [FP Install Dir]\Data\flashpoint.sqlite in a database browser and searching for the ID manually. Alternatively, the **-t** switch can be used, followed by the exact title of an entry: @@ -71,7 +102,7 @@ Or if feeling spontaneous, use the **-r** switch, followed by a library filter t CLIFp play -r game -See the full command/options list for more information. +See the [All Commands/Options](#all-commandsoptions) section for more information. **Direct Execution:** The legacy approach is to use the **run** command with the **--app** and **--param** switches. This will start Flashpoint's webserver and then start the application specified with the provided parameters: @@ -99,111 +130,91 @@ To easily create a share link if you don't already know the UUID of a game, you This will create a share link for that title which will be displayed via a message box and automatically copied to the system clipboard. -If for whatever reason the service through which you wish to share a link does not support links with custom schemes, you can use the **-u** switch to generate a standard "https" link that utilizes a GitHub-hosted redirect page, enabling share links to be provided everywhere. +If for whatever reason the service through which you wish to share a link does not support links with custom schemes, you can use the **-u** switch to generate a standard "https" link that utilizes a GitHub-hosted redirect page, enabling share links to be provided everywhere. > [!IMPORTANT] > You will want to disable the "Register As Protocol Handler" option in the default launcher or else it will replace CLIFp as the "flashpoint" protocol handler every time it's started. ## All Commands/Options -The recommended way to use all switches is to use their short form when the value for the switch has no spaces: + +Most options have short and long forms, which are interchangeable. For options that take a value, a space or **=** can be used between the option and its value, i.e. -i 95b149c2-7f57-4894-b980-6ef03192f79d -and the long form when the value does have spaces +or --msg="I am a message" -though this isn't required as long as quotation and space use is carefully employed. ### Global Options: - - **-h | --help | -?:** Prints usage information - - **-v | --version:** Prints the current version of the tool - - **-q | --quiet:** Silences all non-critical messages - - **-s | --silent:** Silences all messages (takes precedence over quiet mode) - -### Commands: -**link** - Creates a shortcut to a Flashpoint title +- **-h | --help | -?:** Prints usage information +- **-v | --version:** Prints the current version of the tool +- **-q | --quiet:** Silences all non-critical messages +- **-s | --silent:** Silences all messages (takes precedence over quiet mode) -Options: - - **-i | --id:** UUID of title to make a shortcut for - - **-t | --title:** Title to make a shortcut for - - **-T | --title-strict:** Same as **-t**, but only exact matches are considered - - **-s | --subtitle:** Name of additional-app under the title to make a shortcut for. Must be used with **-t**/**-T** - - **-S | --subtitle-strict:** Same as **-s**, but only exact matches are considered - - **-p | --path:** Path to new shortcut. Path's ending with ".lnk" (Windows) or ".desktop" (Linux) will be interpreted as a named shortcut file. Any other path will be interpreted as a directory and the title will automatically be used as the filename - - **-h | --help | -?:** Prints command specific usage information +Every command also has a corresponding help switch for command specific usage information. -Requires: -**-i** or **-t** +### Title Commands: +Many of CLIFp's commands require a game/animation to be specified, which can be done in several ways. These commands, are known as *title commands* and are noted as such below. The explanation for these common options are shown here instead of being repeated under each individually. -Notes: +Title Comamnd Shared Options: +- **-i | --id:** UUID of a game +- **-t | --title:** The title of a game. +- **-T | --title-strict:** Same as **-t**, but only exact matches are considered +- **-s | --subtitle:** Name of an additional-app under a game. Must be used with **-t**/**-T** +- **-S | --subtitle-strict:** Same as **-s**, but only exact matches are considered +- **-r | --random:** Selects a random title from the database. Must be followed by a library filter: `all`/`any`, `game`/`arcade`, or `animation`/`theatre` - - On Linux, when providing a full shortcut path via the **--path** switch, the filename component is re-interpreted as the shortcut's display name and the actual filename is set automatically. +The **-title** and **-subtitle** options are case-insensitive and will match any title that contains the value provided; however, the provided title should match as closely as possible to how it appears within Flashpoint, as checks for close matches are limited due to technical restrictions. If more than one entry is found, a dialog window with more information will be displayed so that the intended title can be selected, though there is a limit to the number of matches. - For example, when specifying: - - CLIFp link -p "~/Desktop/Cool Name.desktop" ... +The **-title-strict** and **-subtitle-strict** options only consider exact matches and are performed slightly faster than their more flexible counterparts. - the display name of the desktop entry will be set to "Cool Name". - - On some Linux desktop environments (i.e. GNOME) the shortcut might need to manually be set to "trusted" in order to be used and displayed correctly after it is created. This option is usually available in the file's right-click context menu. - - See the **play** command notes for information regarding the **t**/**T** and **s**/**S** switches. - --------------------------------------------------------------------------------- +Tip: You can use **-subtitle** with an empty string (i.e. `-s ""`) to see all of the additional-apps for a given title. -**play** - Launch a title and all of it's support applications, in the same manner as using the GUI +### Command List: +**link** - Creates a shortcut to a Flashpoint title Options: - - - **-i | --id:** UUID of title to start - - **-t | --title:** Title to start - - **-T | --title-strict:** Same as **-t**, but only exact matches are considered - - **-s | --subtitle:** Name of additional-app under the title to start. Must be used with **-t**/**-T** - - **-S | --subtitle-strict:** Same as **-s**, but only exact matches are considered - - **-r | --random:** Select a random title from the database to start. Must be followed by a library filter: all/any, game/arcade, animation/theatre - - **-h | --help | -?:** Prints command specific usage information - -Requires: -**-i** or **-t** or **-r** +- [Title Command](#title-commands) options +- **-p | --path:** Path to directory in which to place the shortcut. Prompts if not provided. +- **-n | --name:** Name of the shortcut. Defaults to the name of the title Notes: + - On some Linux desktop environments (i.e. GNOME) the shortcut might need to manually be set to "trusted" in order to be used and displayed correctly after it is created. This option is usually available in the file's right-click context menu. -The **-t** and **-s** switches are case-insensitive and will match any title that contains the value provided. If more than one result is found, a dialog will be presented that allows for selected the desired title; however, there is a limit to the number of matches. +-------------------------------------------------------------------------------- -Using the **-T** and **-S** switches only consider exact matches and are performed slightly faster than their more flexible counterparts. +**play** - Launch a game/animation. The bread and butter of this application. -Tip: You can use **-s** with an empty string (i.e. `-s ""`) to see all of the additional-apps for a given title. +Options: + +- [Title Command](#title-commands) options -------------------------------------------------------------------------------- - + **prepare** - Initializes Flashpoint for playing the provided Data Pack based title by UUID. If the title does not use a Data Pack this command has no effect. Options: - - **-i | --id:** UUID of title to prepare - - **-t | --title:** Title to prepare - - **-h | --help | -?:** Prints command specific usage information - -Requires: -**-i** or **-t** +- [Title Command](#title-commands) options -------------------------------------------------------------------------------- - + **run** - Start Flashpoint's webserver and then execute the provided application Options: - - **-a | --app:** Relative (to Flashpoint Directory) path of application to launch - - **-p | --param:** Command-line parameters to use when starting the application - - **-h | --help | -?:** Prints command specific usage information +- **-a | --app:** Relative (to Flashpoint Directory) path of application to launch +- **-p | --param:** Command-line parameters to use when starting the application Requires: -**-a** +**-a** -Notes: +Notes: When using the **--exe** and **--param** switches all quotes that are part of the input itself must be escaped for the command to be passed correctly. For example, the launch command "http://website.com" -switch "value" must be specified as - + --param="\"http://website.com\" -switch \"value\"" Additionally, any characters with special meaning to the Windows shell that are not within at least one level of quote pairs must also be escaped (this can be avoided as long as you wrap the entire value of the switch in quotes as shown above): @@ -217,38 +228,32 @@ See http://www.robvanderwoude.com/escapechars.php for more information. **share** - Generates a URL for starting a Flashpoint title that can be shared to other users. Options: - - **-i | --id:** UUID of title to make a share link for - - **-t | --title:** Title to make a share link for - - **-T | --title-strict:** Same as **-t**, but only exact matches are considered - - **-s | --subtitle:** Name of additional-app under the title to make a share link for. Must be used with **-t**/**-T** - - **-S | --subtitle-strict:** Same as **-s**, but only exact matches are considered - - **-u | --universal:** Creates a standard HTTPS link that utilizes a redirect page. May be easier to share on some platforms. + - [Title Command](#title-commands) options + - **-u | --universal:** Creates a standard HTTPS link that utilizes a redirect page. May be easier to share on some platforms. - **-c | --configure:** Registers CLIFp as the default handler for "flashpoint" protocol links. - **-C | --unconfigure:** Removes CLIFp as the default handler for "flashpoint" protocol links. - - **-h | --help | -?:** Prints command specific usage information -Requires: -**-i**, **-t** or **-c** - -Notes: +Notes: + - No title is required when using the **--configure*** option - By default, the standard Flashpoint launcher is registered to handle share links; therefore, its "Register As Protocol Handler" option should likely be disabled if you intend to use CLIFp instead. - - See the **play** command notes for information regarding the **t**/**T** and **s**/**S** switches. -------------------------------------------------------------------------------- **show** - Display a message or extra folder Options: - - **-m | --msg:** Displays an pop-up dialog with the supplied message. Used primarily for some additional apps - - **-e | --extra:** Opens an explorer window to the specified extra. Used primarily for some additional apps - - **-h | --help | -?:** Prints command specific usage information + - **-m | --msg:** Displays an pop-up dialog with the supplied message. Used primarily for some additional apps + - **-e | --extra:** Opens an explorer window to the specified extra. Used primarily for some additional apps + - **-h | --help | -?:** Prints command specific usage information Requires: -**-m** or **-e** +**-m** or **-e** + +-------------------------------------------------------------------------------- + + **update** - Check for and optional apply the latest update -### Remarks -With any use of the **--title**/**--subtitle** options for the commands that support them, the provided title should match as closely as possible to how it appears within Flashpoint, as checks for close matches are limited due to technical restrictions. If more than one entry is found, a dialog window with more information will be displayed so that the intended title can be selected. ## Other Features CLIFp displays a system tray icon so that one can be sure it is still running. This icon also will display basic status messages when clicked on and features a context menu with an option to exit at any time. diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 6895803..af6d3df 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -2,6 +2,7 @@ # Pre-configure target set(CLIFP_SOURCE + kernel/buildinfo.h kernel/core.h kernel/core.cpp kernel/driver.h @@ -22,6 +23,8 @@ set(CLIFP_SOURCE command/c-share.cpp command/c-show.cpp command/c-show.h + command/c-update.h + command/c-update.cpp command/title-command.h command/title-command.cpp task/task.h @@ -126,12 +129,29 @@ ob_add_cpp_vars(${APP_TARGET_NAME} NAME "project_vars" PREFIX "PROJECT_" VARS - VERSION_STR "\"${PROJECT_VERSION}\"" + VERSION_STR "\"${PROJECT_VERSION_VERBOSE}\"" SHORT_NAME "\"${APP_ALIAS_NAME}\"" TARGET_FP_VER_PFX_STR "\"${TARGET_FP_VERSION_PREFIX}\"" APP_NAME "\"${PROJECT_FORMAL_NAME}\"" ) +## Add build info +if(BUILD_SHARED_LIBS) + set(link_str "Shared") +else() + set(link_str "Static") +endif() + +ob_add_cpp_vars(${APP_TARGET_NAME} + NAME "_buildinfo" + PREFIX "BUILDINFO_" + VARS + SYSTEM "\"${CMAKE_SYSTEM_NAME}\"" + LINKAGE "\"${link_str}\"" + COMPILER "u\"${CMAKE_CXX_COMPILER_ID}\"_s" + COMPILER_VER_STR "u\"${CMAKE_CXX_COMPILER_VERSION}\"_s" +) + ## Add exe details on Windows if(CMAKE_SYSTEM_NAME STREQUAL Windows) # Set target exe details diff --git a/app/src/command/c-link.cpp b/app/src/command/c-link.cpp index 93438f3..d6e0489 100644 --- a/app/src/command/c-link.cpp +++ b/app/src/command/c-link.cpp @@ -54,7 +54,7 @@ Qx::Error CLink::perform() // Get database Fp::Db* database = mCore.fpInstall().database(); - // Get entry (also confirms that ID is present in database) + // Get entry (also confirms that ID is present in database, which is why we do this even if a custom name is set) std::variant entry_v; Fp::DbError dbError = database->getEntry(entry_v, shortcutId); if(dbError.isValid()) @@ -66,7 +66,7 @@ Qx::Error CLink::perform() if(std::holds_alternative(entry_v)) { Fp::Game game = std::get(entry_v); - shortcutName = Qx::kosherizeFileName(game.title()); + shortcutName = game.title(); } else if(std::holds_alternative(entry_v)) { @@ -82,38 +82,28 @@ Qx::Error CLink::perform() Q_ASSERT(std::holds_alternative(entry_v)); Fp::Game parent = std::get(entry_v); - shortcutName = Qx::kosherizeFileName(parent.title() + u" ("_s + addApp.name() + u")"_s); + shortcutName = parent.title() + u" ("_s + addApp.name() + u")"_s; } else qCritical("Invalid variant state for std::variant."); + // Override shortcut name with user input + if(mParser.isSet(CL_OPTION_NAME)) + shortcutName = mParser.value(CL_OPTION_NAME); + // Get shortcut path if(mParser.isSet(CL_OPTION_PATH)) - { - QFileInfo inputPathInfo(mParser.value(CL_OPTION_PATH)); - if(inputPathInfo.suffix() == shortcutExtension()) // Path is file - { - mCore.logEvent(NAME, LOG_EVENT_FILE_PATH); - shortcutDir = inputPathInfo.absoluteDir(); - shortcutName = inputPathInfo.baseName(); - } - else // Path is directory - { - mCore.logEvent(NAME, LOG_EVENT_DIR_PATH); - shortcutDir = QDir(inputPathInfo.absoluteFilePath()); - } - } + shortcutDir = mParser.value(CL_OPTION_PATH); else { mCore.logEvent(NAME, LOG_EVENT_NO_PATH); // Prompt user for path - Core::SaveFileRequest sfr{ + Core::ExistingDirRequest edr{ .caption = DIAG_CAPTION, - .dir = QDir::homePath() + u"/Desktop/"_s + shortcutName, - .filter = u"Shortcuts (*. "_s + shortcutExtension() + u")"_s + .dir = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation) }; - QString selectedPath = mCore.requestSaveFilePath(sfr); + QString selectedPath = mCore.requestExistingDirPath(edr); if(selectedPath.isEmpty()) { @@ -122,13 +112,8 @@ Qx::Error CLink::perform() } else { - if(!selectedPath.endsWith(u"."_s + shortcutExtension(), Qt::CaseInsensitive)) - selectedPath += u"."_s + shortcutExtension(); - mCore.logEvent(NAME, LOG_EVENT_SEL_PATH.arg(QDir::toNativeSeparators(selectedPath))); - QFileInfo pathInfo(selectedPath); - shortcutDir = pathInfo.absoluteDir(); - shortcutName = pathInfo.baseName(); + shortcutDir = selectedPath; } } diff --git a/app/src/command/c-link.h b/app/src/command/c-link.h index 3779dce..70778af 100644 --- a/app/src/command/c-link.h +++ b/app/src/command/c-link.h @@ -60,8 +60,6 @@ class CLink : public TitleCommand static inline const QString DIAG_CAPTION = u"Select a shortcut destination..."_s; // Logging - Messages - static inline const QString LOG_EVENT_FILE_PATH = u"Shortcut path provided is for a file"_s; - static inline const QString LOG_EVENT_DIR_PATH = u"Shortcut path provided is for a folder"_s; static inline const QString LOG_EVENT_NO_PATH = u"No shortcut path provided, user will be prompted"_s; static inline const QString LOG_EVENT_SEL_PATH = u"Shortcut path selected: %1"_s; static inline const QString LOG_EVENT_DIAG_CANCEL = u"Shortcut path selection canceled."_s; @@ -71,14 +69,17 @@ class CLink : public TitleCommand // Command line option strings static inline const QString CL_OPT_PATH_S_NAME = u"p"_s; static inline const QString CL_OPT_PATH_L_NAME = u"path"_s; - static inline const QString CL_OPT_PATH_DESC = u"Path to new shortcut. Path's ending with "".lnk""//"".desktop"" will be interpreted as a named shortcut file. " - "Any other path will be interpreted as a directory and the title will automatically be used " - "as the filename"_s; + static inline const QString CL_OPT_PATH_DESC = u"Path to a directory for the new shortcut"_s; + + static inline const QString CL_OPT_NAME_S_NAME = u"n"_s; + static inline const QString CL_OPT_NAME_L_NAME = u"name"_s; + static inline const QString CL_OPT_NAME_DESC = u"Name of the shortcut. Defaults to the name of the title"_s; // Command line options static inline const QCommandLineOption CL_OPTION_PATH{{CL_OPT_PATH_S_NAME, CL_OPT_PATH_L_NAME}, CL_OPT_PATH_DESC, u"path"_s}; // Takes value + static inline const QCommandLineOption CL_OPTION_NAME{{CL_OPT_NAME_S_NAME, CL_OPT_NAME_L_NAME}, CL_OPT_NAME_DESC, u"name"_s}; // Takes value - static inline const QList CL_OPTIONS_SPECIFIC{&CL_OPTION_PATH}; + static inline const QList CL_OPTIONS_SPECIFIC{&CL_OPTION_PATH, &CL_OPTION_NAME}; static inline const QSet CL_OPTIONS_REQUIRED{}; public: @@ -93,7 +94,6 @@ class CLink : public TitleCommand //-Instance Functions------------------------------------------------------------------------------------------------------ private: Qx::Error createShortcut(const QString& name, const QDir& dir, QUuid id); - QString shortcutExtension() const; protected: QList options() override; diff --git a/app/src/command/c-link_linux.cpp b/app/src/command/c-link_linux.cpp index e4aeea6..9fb1c24 100644 --- a/app/src/command/c-link_linux.cpp +++ b/app/src/command/c-link_linux.cpp @@ -34,8 +34,7 @@ Qx::Error CLink::createShortcut(const QString& name, const QDir& dir, QUuid id) ade.setComment(u"Generated by "_s PROJECT_SHORT_NAME " " PROJECT_VERSION_STR); // Create entry - QString filename = u"org.flashpoint.clifp."_s + id.toString(QUuid::WithoutBraces) + - '.' + shortcutExtension(); + QString filename = u"org.flashpoint.clifp."_s + id.toString(QUuid::WithoutBraces) + u".desktop"_s; QString fullEntryPath = dir.absoluteFilePath(filename); Qx::IoOpReport writeReport = Qx::DesktopEntry::writeToDisk(fullEntryPath, &ade); @@ -51,5 +50,3 @@ Qx::Error CLink::createShortcut(const QString& name, const QDir& dir, QUuid id) // Return success return CLinkError(); } - -QString CLink::shortcutExtension() const { return u"desktop"_s; }; diff --git a/app/src/command/c-link_win.cpp b/app/src/command/c-link_win.cpp index 543adb6..0999d70 100644 --- a/app/src/command/c-link_win.cpp +++ b/app/src/command/c-link_win.cpp @@ -16,6 +16,8 @@ //Private: Qx::Error CLink::createShortcut(const QString& name, const QDir& dir, QUuid id) { + QString filename = Qx::kosherizeFileName(name); + // Create shortcut properties Qx::ShortcutProperties sp; sp.target = CLIFP_PATH; @@ -23,7 +25,7 @@ Qx::Error CLink::createShortcut(const QString& name, const QDir& dir, QUuid id) sp.comment = name; // Create shortcut - QString fullShortcutPath = dir.absolutePath() + '/' + name + '.' + shortcutExtension(); + QString fullShortcutPath = dir.absolutePath() + '/' + filename + u".lnk"_s; Qx::SystemError shortcutError = Qx::createShortcut(fullShortcutPath, sp); // Check for creation failure @@ -38,5 +40,3 @@ Qx::Error CLink::createShortcut(const QString& name, const QDir& dir, QUuid id) // Return success return CLinkError(); } - -QString CLink::shortcutExtension() const { return u"lnk"_s; }; diff --git a/app/src/command/c-play.h b/app/src/command/c-play.h index 6c78d5d..44a5148 100644 --- a/app/src/command/c-play.h +++ b/app/src/command/c-play.h @@ -80,7 +80,7 @@ class CPlay : public TitleCommand public: // Meta static inline const QString NAME = u"play"_s; - static inline const QString DESCRIPTION = u"Launch a title and all of it's support applications, in the same manner as using the GUI"_s; + static inline const QString DESCRIPTION = u"Launch a game/animation"_s; //-Constructor---------------------------------------------------------------------------------------------------------- public: diff --git a/app/src/command/c-update.cpp b/app/src/command/c-update.cpp new file mode 100644 index 0000000..8ed4f8f --- /dev/null +++ b/app/src/command/c-update.cpp @@ -0,0 +1,446 @@ +// Unit Include +#include "c-update.h" + +// Qt Includes +#include +#include + +// Qx Includes +#include +#include + +// Project Includes +#include "task/t-download.h" +#include "task/t-extract.h" +#include "task/t-exec.h" +#include "utility.h" + +//=============================================================================================================== +// CUpdateError +//=============================================================================================================== + +//-Constructor------------------------------------------------------------- +//Private: +CUpdateError::CUpdateError(Type t, const QString& s, Qx::Severity sv) : + mType(t), + mSpecific(s), + mSeverity(sv) +{} + +//-Instance Functions------------------------------------------------------------- +//Public: +bool CUpdateError::isValid() const { return mType != NoError; } +QString CUpdateError::specific() const { return mSpecific; } +CUpdateError::Type CUpdateError::type() const { return mType; } + +//Private: +Qx::Severity CUpdateError::deriveSeverity() const { return mSeverity; } +quint32 CUpdateError::deriveValue() const { return mType; } +QString CUpdateError::derivePrimary() const { return ERR_STRINGS.value(mType); } +QString CUpdateError::deriveSecondary() const { return mSpecific; } + +//=============================================================================================================== +// CUpdate +//=============================================================================================================== + +//-Constructor------------------------------------------------------------- +//Public: +CUpdate::CUpdate(Core& coreRef) : Command(coreRef) {} + +//-Class Functions---------------------------------------------------------------- +QDir CUpdate::updateCacheDir() +{ + static QDir ucd(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u"/update"_s); + return ucd; +} + +QDir CUpdate::updateDownloadDir() { return updateCacheDir().absoluteFilePath(u"download"_s); } +QDir CUpdate::updateDataDir() { return updateCacheDir().absoluteFilePath(u"data"_s); } +QDir CUpdate::updateBackupDir() { return updateCacheDir().absoluteFilePath(u"backup"_s); } + +QString CUpdate::sanitizeCompiler(QString cmp) +{ + if(cmp.contains("clang", Qt::CaseInsensitive)) + cmp = "clang++"; + else if(cmp.contains("gnu", Qt::CaseInsensitive)) + cmp = "gcc++"; + else if(cmp.contains("msvc", Qt::CaseInsensitive)) + cmp = "msvc"; + else + cmp = cmp.toLower(); + + return cmp; +} + +QString CUpdate::substitutePathNames(const QString& path, QStringView binName, QStringView appName) +{ + static const QString stdBin = u"bin"_s; + static const QString installerName = CLIFP_CUR_APP_FILENAME; + + QString subbed = path; + + if(subbed.startsWith(stdBin)) + subbed = subbed.mid(stdBin.size()).prepend(binName); + if(subbed.endsWith(installerName)) + { + subbed.chop(installerName.size()); + subbed.append(appName); + } + + return subbed; +} + +Qx::IoOpReport CUpdate::determineNewFiles(QStringList& files, const QDir& sourceRoot) +{ + files = {}; + + QStringList subDirs = {u"bin"_s}; + for(const auto& sd : {u"lib"_s, u"plugins"_s}) + if(sourceRoot.exists(sd)) + subDirs += sd; + + for(const auto& sd : qAsConst(subDirs)) + { + QStringList subFiles; + Qx::IoOpReport r = Qx::dirContentList(subFiles, sourceRoot.absoluteFilePath(sd), {}, QDir::NoDotAndDotDot | QDir::Files, + QDirIterator::Subdirectories, Qx::PathType::Relative); + if(r.isFailure()) + return r; + + for(auto& p : subFiles) + p.prepend(sd + '/'); + + files += subFiles; + } + + files.removeIf([](const QString& s){ return s == u"bin/"_s + CLIFP_CUR_APP_BASENAME + u".log"_s; }); + + return Qx::IoOpReport(Qx::IO_OP_ENUMERATE, Qx::IO_SUCCESS, sourceRoot); +} + +CUpdate::UpdateTransfers CUpdate::determineTransfers(const QStringList& files, const TransferSpecs& specs) +{ + UpdateTransfers transfers; + + for(const QString& file : files) + { + QString installPath = specs.installRoot.filePath(substitutePathNames(file, specs.binName, specs.appName)); + QString updatePath = specs.updateRoot.filePath(file); + QString backupPath = specs.backupRoot.filePath(file); + transfers.install << FileTransfer{.source = updatePath, .dest = installPath}; + transfers.backup << FileTransfer{.source = installPath, .dest = backupPath}; + } + + return transfers; +} + +//Public: +bool CUpdate::isUpdateCacheClearable() { return updateCacheDir().exists() && !smPersistCache; } + +CUpdateError CUpdate::clearUpdateCache() +{ + QDir uc = updateCacheDir(); + bool removed = uc.exists() && uc.removeRecursively(); + return CUpdateError(removed ? CUpdateError::NoError : CUpdateError::CacheClearFail, {}, Qx::Warning); +} + +//-Instance Functions------------------------------------------------------------- +//Private: +CUpdateError CUpdate::getLatestReleaseData(ReleaseData& data) const +{ + // Get latest release data via GitHub REST + + // Prepare + QEventLoop waiter; // Generally avoid nested event loops, but safe here as re-entry is impossible + QNetworkAccessManager nm; + nm.setAutoDeleteReplies(true); + nm.setTransferTimeout(2000); + QNetworkRequest req(u"https://api.github.com/repos/oblivioncth/CLIFp/releases/latest"_s); + req.setRawHeader("Accept"_ba, "application/vnd.github+json"_ba); + + // Get + QNetworkReply* reply = nm.get(req); + + // Result handler + CUpdateError restError; + QByteArray response; + //clazy:excludeall=lambda-in-connect + QObject::connect(reply, &QNetworkReply::finished, &mCore, [&]{ + if(reply->error() == QNetworkReply::NoError) + response = reply->readAll(); + else + restError = CUpdateError(CUpdateError::ConnectionError, reply->errorString()); + + waiter.quit(); + }); + + // Wait on result + waiter.exec(); + if(restError.isValid()) + return restError; + + // Parse data + QJsonParseError jpe; + QJsonDocument jd = QJsonDocument::fromJson(response, &jpe); + if(jpe.error != QJsonParseError::NoError) + return CUpdateError(CUpdateError::InvalidUpdateData, jpe.errorString()); + if(Qx::JsonError je = Qx::parseJson(data, jd); je.isValid()) + return CUpdateError(CUpdateError::InvalidUpdateData, je.action()); + + return CUpdateError(); +} + +QString CUpdate::getTargetAssetName(const QString& tagName) const +{ + BuildInfo bi = mCore.buildInfo(); + + QString cmpStr = sanitizeCompiler(bi.compiler); + if(bi.system == BuildInfo::Linux) + cmpStr = RELEASE_ASSET_LINUX_CMP_TEMPLATE.arg(cmpStr, QString::number(bi.compilerVersion.majorVersion())); + + return RELEASE_ASSET_MAIN_TEMPLATE.arg( + tagName, + ENUM_NAME(bi.system), + ENUM_NAME(bi.linkage), + cmpStr + ); +} + +CUpdateError CUpdate::handleTransfers(const UpdateTransfers& transfers) const +{ + auto doTransfer = [&](const FileTransfer& ft, bool mkpath, bool move, bool overwrite){ + mCore.logEvent(NAME, LOG_EVENT_FILE_TRANSFER.arg(ft.source, ft.dest)); + + if(mkpath) + { + QDir destDir(QFileInfo(ft.dest).absolutePath()); + if(!destDir.mkpath(u"."_s)) + return false; + } + + if(overwrite && QFile::exists(ft.dest) && !QFile::remove(ft.dest)) + return false; + + return move ? QFile::rename(ft.source, ft.dest) : QFile::copy(ft.source, ft.dest); + }; + + // Backup, and note for restore + mCore.logEvent(NAME, LOG_EVENT_BACKUP_FILES); + QList restoreTransfers; + QScopeGuard restoreOnFail([&]{ + if(!restoreTransfers.isEmpty()) + { + mCore.logEvent(NAME, LOG_EVENT_RESTORE_FILES); + for(const auto& t : restoreTransfers) doTransfer(t, false, true, true); + } + }); + + for(const auto& ft : transfers.backup) + { + if(!doTransfer(ft, true, true, true)) + { + CUpdateError err(CUpdateError::TransferFail, ft.dest); + mCore.postError(NAME, err); + return err; + } + restoreTransfers << FileTransfer{.source = ft.dest, .dest = ft.source}; + } + + // Install + mCore.logEvent(NAME, LOG_EVENT_INSTALL_FILES); + for(const auto& ft : transfers.install) + { + if(!doTransfer(ft, true, false, false)) + { + CUpdateError err(CUpdateError::TransferFail, ft.dest); + mCore.postError(NAME, err); + return err; + } + } + restoreOnFail.dismiss(); + + return CUpdateError(); +} + +CUpdateError CUpdate::checkAndPrepareUpdate() const +{ + // This command had auto instance block disabled so we do this manually here + if(!mCore.blockNewInstances()) + { + CUpdateError err(CUpdateError::AlreadyOpen); + mCore.postError(NAME, err); + return err; + } + + // Check for update + mCore.setStatus(STATUS, STATUS_CHECKING); + mCore.logEvent(NAME, LOG_EVENT_CHECKING_FOR_NEWER_VERSION); + + // Get new release data + ReleaseData rd; + if(CUpdateError ue = getLatestReleaseData(rd); ue.isValid()) + { + mCore.postError(NAME, ue); + return ue; + } + + // Check if newer + QVersionNumber currentVersion = QVersionNumber::fromString(QCoreApplication::applicationVersion().mid(1)); // Drops 'v' + Q_ASSERT(!currentVersion.isNull()); + QVersionNumber newVersion = QVersionNumber::fromString(rd.tag_name.mid(1)); // Drops 'v' + if(newVersion.isNull()) + { + CUpdateError err(CUpdateError::InvalidReleaseVersion); + mCore.postError(NAME, err); + return err; + } + + if(newVersion <= currentVersion) + { + mCore.postMessage(Message{.text = MSG_NO_UPDATES}); + mCore.logEvent(NAME, MSG_NO_UPDATES); + return CUpdateError(); + } + mCore.logEvent(NAME, LOG_EVENT_UPDATE_AVAILABLE.arg(rd.tag_name)); + + // Get current build info + BuildInfo bi = mCore.buildInfo(); + + // Check for applicable artifact + QString targetAsset = getTargetAssetName(rd.tag_name); + + auto aItr = std::find_if(rd.assets.cbegin(), rd.assets.cend(), [&](const auto& a){ + return a.name == targetAsset; + }); + + if(aItr == rd.assets.cend()) + { + mCore.postError(NAME, Qx::GenericError(Qx::Warning, 12181, WRN_NO_MATCHING_BUILD_P, WRN_NO_MATCHING_BUILD_S)); + return CUpdateError(); + } + + if(mCore.requestQuestionAnswer(QUES_UPDATE.arg(rd.name))) + { + mCore.logEvent(NAME, LOG_EVENT_UPDATE_ACCEPED); + + // Queue update + QDir uDownloadDir = updateDownloadDir(); + QDir uDataDir = updateDataDir(); + + QString tempName = u"clifp_update.zip"_s; + TDownload* downloadTask = new TDownload(&mCore); + downloadTask->setStage(Task::Stage::Primary); + downloadTask->setTargetFile(aItr->browser_download_url); + downloadTask->setDestinationPath(uDownloadDir.absolutePath()); + downloadTask->setDestinationFilename(tempName); + mCore.enqueueSingleTask(downloadTask); + + TExtract* extractTask = new TExtract(&mCore); + extractTask->setStage(Task::Stage::Primary); + extractTask->setPackPath(uDownloadDir.absoluteFilePath(tempName)); + extractTask->setDestinationPath(uDataDir.absolutePath()); + mCore.enqueueSingleTask(extractTask); + + TExec* execTask = new TExec(&mCore); + execTask->setStage(Task::Stage::Primary); + execTask->setIdentifier(UPDATE_STAGE_NAME); + QString newAppExecPath = uDataDir.absolutePath() + u"/bin"_s; + execTask->setExecutable(newAppExecPath + '/' + CLIFP_CANONICAL_APP_FILNAME); + execTask->setDirectory(newAppExecPath); + execTask->setParameters(QStringList{u"update"_s, u"--install"_s, CLIFP_PATH}); + execTask->setProcessType(TExec::ProcessType::Detached); + mCore.enqueueSingleTask(execTask); + + // Maintain update cache until installer runs + smPersistCache = true; + } + else + mCore.logEvent(NAME, LOG_EVENT_UPDATE_REJECTED); + + return CUpdateError(); +} + +Qx::Error CUpdate::installUpdate(const QFileInfo& existingAppInfo) const +{ + mCore.setStatus(STATUS, STATUS_INSTALLING); + + // Wait for previous process to close, lock instance afterwards + static const int totalGrace = 2000; + static const int step = 500; + int currentGrace = 0; + bool haveLock = false; + + do + { + mCore.logEvent(NAME, LOG_EVENT_WAITING_ON_OLD_CLOSE.arg(totalGrace - currentGrace)); + QThread::msleep(step); + currentGrace += step; + haveLock = mCore.blockNewInstances(); + } + while(!haveLock && currentGrace < totalGrace); + + // TODO: Allow user retry here (i.e. they close the process manually) + if(!haveLock) + { + CUpdateError err(CUpdateError::OldProcessNotFinished, "Aborting update."); + mCore.postError(NAME, err); + return err; + } + + //-Install update------------------------------------------------------------ + mCore.logEvent(NAME, LOG_EVENT_INSTALLING_UPDATE); + + // Ensure old executable exists where expected + if(!existingAppInfo.exists()) + { + CUpdateError err(CUpdateError::InvalidPath, "Missing " + existingAppInfo.absoluteFilePath()); + mCore.postError(NAME, err); + return err; + } + + // Note structure + TransferSpecs ts{ + .updateRoot = updateDataDir(), + .installRoot = QDir(QDir::cleanPath(existingAppInfo.absoluteFilePath() + "/../..")), + .backupRoot = updateBackupDir(), + .appName = existingAppInfo.fileName(), + .binName = existingAppInfo.absoluteDir().dirName() + }; + + // Determine transfers + QStringList updateFiles; + if(Qx::IoOpReport rep = determineNewFiles(updateFiles, ts.updateRoot); rep.isFailure()) + { + mCore.postError(NAME, rep); + return rep; + } + + UpdateTransfers updateTransfers = determineTransfers(updateFiles, ts); + + // Transfer + if(CUpdateError err = handleTransfers(updateTransfers); err.isValid()) + return err; + + // Success + mCore.logEvent(NAME, MSG_UPDATE_COMPLETE); + mCore.postMessage(Message{.text = MSG_UPDATE_COMPLETE}); + return CUpdateError(); +} + +//Protected: +QList CUpdate::options() { return CL_OPTIONS_SPECIFIC + Command::options(); } +QString CUpdate::name() { return NAME; } + +Qx::Error CUpdate::perform() +{ + /* Persist cache during setup so that files remain for install, and during install (so always) + * since the installer cannot delete itself while it's running + */ + smPersistCache = true; + return mParser.isSet(CL_OPTION_INSTALL) ? installUpdate(QFileInfo(mParser.value(CL_OPTION_INSTALL))) : + checkAndPrepareUpdate(); +} + +//Public: +bool CUpdate::requiresFlashpoint() const { return false; } +bool CUpdate::autoBlockNewInstances() const { return false; } diff --git a/app/src/command/c-update.h b/app/src/command/c-update.h new file mode 100644 index 0000000..24088cc --- /dev/null +++ b/app/src/command/c-update.h @@ -0,0 +1,209 @@ +#ifndef CUPDATE_H +#define CUPDATE_H + +// Qx Includes +#include + +// Project Includes +#include "command/command.h" + +class QX_ERROR_TYPE(CUpdateError, "CUpdateError", 1218) +{ + friend class CUpdate; +//-Class Enums------------------------------------------------------------- +public: + enum Type + { + NoError, + AlreadyOpen, + ConnectionError, + InvalidUpdateData, + InvalidReleaseVersion, + OldProcessNotFinished, + InvalidPath, + TransferFail, + CacheClearFail + }; + +//-Class Variables------------------------------------------------------------- +private: + static inline const QHash ERR_STRINGS{ + {NoError, u""_s}, + {AlreadyOpen, u"Cannot update when another instance of CLIFp is running."_s}, + {ConnectionError, u"Failed to query the update server."_s}, + {InvalidUpdateData, u"The update server responded with unrecognized data."_s}, + {InvalidReleaseVersion, u"The latest release has an invalid version."_s}, + {OldProcessNotFinished, u"The old version is still running."_s}, + {InvalidPath, u"An update path is invalid."_s}, + {TransferFail, u"File transfer operation failed."_s}, + {CacheClearFail, u"Failed to clear the update cache."_s} + }; + +//-Instance Variables------------------------------------------------------------- +private: + Type mType; + QString mSpecific; + Qx::Severity mSeverity; + +//-Constructor------------------------------------------------------------- +private: + CUpdateError(Type t = NoError, const QString& s = {}, Qx::Severity sv = Qx::Critical); + +//-Instance Functions------------------------------------------------------------- +public: + bool isValid() const; + Type type() const; + QString specific() const; + +private: + Qx::Severity deriveSeverity() const override; + quint32 deriveValue() const override; + QString derivePrimary() const override; + QString deriveSecondary() const override; +}; + +class CUpdate : public Command +{ +//-Class Structs------------------------------------------------------------------------------------------------------ +private: + struct ReleaseAsset + { + QString name; + QString browser_download_url; + + QX_JSON_STRUCT( + name, + browser_download_url + ); + }; + + struct ReleaseData + { + QString name; + QString tag_name; + QList assets; + + QX_JSON_STRUCT( + name, + tag_name, + assets + ); + }; + + struct FileTransfer + { + QString source; + QString dest; + }; + + struct TransferSpecs + { + QDir updateRoot; + QDir installRoot; + QDir backupRoot; + QString appName; + QString binName; + }; + + struct UpdateTransfers + { + QList install; + QList backup; + }; + +//-Class Variables------------------------------------------------------------------------------------------------------ +private: + // Status + static inline const QString STATUS = u"Updating"_s; + static inline const QString STATUS_CHECKING = u"Checking..."_s; + static inline const QString STATUS_DOWNLOADING = u"Downloading..."_s; + static inline const QString STATUS_INSTALLING = u"Installing..."_s; + + // Message + static inline const QString MSG_NO_UPDATES = u"No updates available."_s; + static inline const QString QUES_UPDATE = u"\"%1\" is available.\n\nUpdate?"_s; + static inline const QString MSG_UPDATE_COMPLETE = u"Update installed successfully."_s; + + // Error + static inline const QString WRN_NO_MATCHING_BUILD_P = u"A newer version is available, but without any assets that match current build specifications."_s; + static inline const QString WRN_NO_MATCHING_BUILD_S = u"Update manually at GitHub."_s; + + // Log - Prepare + static inline const QString LOG_EVENT_CHECKING_FOR_NEWER_VERSION = u"Checking if a newer release is available..."_s; + static inline const QString LOG_EVENT_UPDATE_AVAILABLE = u"Update available (%1)."_s; + static inline const QString LOG_EVENT_UPDATE_ACCEPED = u"Queuing update..."_s; + static inline const QString LOG_EVENT_UPDATE_REJECTED = u"Update rejected"_s; + + // Log - Install + static inline const QString LOG_EVENT_WAITING_ON_OLD_CLOSE = u"Waiting for bootstrap process to close (%1ms reamining)..."_s; + static inline const QString LOG_EVENT_INSTALLING_UPDATE = u"Installing update..."_s; + static inline const QString LOG_EVENT_FILE_TRANSFER = u"Transferring \"%1\" to \"%2\""_s; + static inline const QString LOG_EVENT_BACKUP_FILES = u"Backing up original files..."_s; + static inline const QString LOG_EVENT_RESTORE_FILES = u"Restoring original files..."_s; + static inline const QString LOG_EVENT_INSTALL_FILES = u"Installing new files..."_s; + + // Command line option strings + static inline const QString CL_OPT_INSTALL_L_NAME = u"install"_s; + static inline const QString CL_OPT_INSTALL_DESC = u""_s; + + // Command line options + static inline const QCommandLineOption CL_OPTION_INSTALL{{CL_OPT_INSTALL_L_NAME}, CL_OPT_INSTALL_DESC, u"install"_s}; // Takes value + static inline const QList CL_OPTIONS_SPECIFIC{&CL_OPTION_INSTALL}; + + // General + static inline const QString RELEASE_ASSET_MAIN_TEMPLATE = u"CLIFp_%1_%2_%3_x64.%4.zip"_s; + static inline const QString RELEASE_ASSET_LINUX_CMP_TEMPLATE = u"%1++-%2"_s; + static inline const QString UPDATE_STAGE_NAME = u"CLIFp Updater"_s; + + // Cache + static inline constinit bool smPersistCache = false; + +public: + // Meta + static inline const QString NAME = u"update"_s; + static inline const QString DESCRIPTION = u"Check for and optionally install updates."_s; + +//-Constructor---------------------------------------------------------------------------------------------------------- +public: + CUpdate(Core& coreRef); + +//-Class Functions------------------------------------------------------------------------------------------------------ +private: + // Path + static QDir updateCacheDir(); + static QDir updateDownloadDir(); + static QDir updateDataDir(); + static QDir updateBackupDir(); + + // Adjustment + static QString sanitizeCompiler(QString cmp); + static QString substitutePathNames(const QString& path, QStringView binName, QStringView appName); + + // Work + static Qx::IoOpReport determineNewFiles(QStringList& files, const QDir& sourceRoot); + static UpdateTransfers determineTransfers(const QStringList& files, const TransferSpecs& specs); + +public: + static bool isUpdateCacheClearable(); + static CUpdateError clearUpdateCache(); + +//-Instance Functions------------------------------------------------------------------------------------------------------ +private: + CUpdateError getLatestReleaseData(ReleaseData& data) const; + QString getTargetAssetName(const QString& tagName) const; + CUpdateError handleTransfers(const UpdateTransfers& transfers) const; + CUpdateError checkAndPrepareUpdate() const; + Qx::Error installUpdate(const QFileInfo& existingAppInfo) const; + +protected: + QList options() override; + QString name() override; + Qx::Error perform() override; + +public: + bool requiresFlashpoint() const override; + bool autoBlockNewInstances() const override; +}; +REGISTER_COMMAND(CUpdate::NAME, CUpdate, CUpdate::DESCRIPTION); + +#endif // CUPDATE_H diff --git a/app/src/command/command.cpp b/app/src/command/command.cpp index 30e133d..010fd9a 100644 --- a/app/src/command/command.cpp +++ b/app/src/command/command.cpp @@ -83,7 +83,7 @@ CommandError Command::parse(const QStringList& commandLine) if(!optionsStr.isEmpty()) optionsStr += ' '; // Space after every switch except first one - optionsStr += u"--"_s + (*clOption).names().at(1); // Always use long name + optionsStr += u"--"_s + (*clOption).names().constLast(); // Always use long name // Add value of switch if it takes one if(!(*clOption).valueName().isEmpty()) @@ -175,6 +175,10 @@ void Command::showHelp() QList Command::options() { return CL_OPTIONS_STANDARD; } QSet Command::requiredOptions() { return {}; } +//Public: +bool Command::requiresFlashpoint() const { return true; } +bool Command::autoBlockNewInstances() const { return true; } + Qx::Error Command::process(const QStringList& commandLine) { // Parse and check for valid arguments @@ -197,3 +201,5 @@ Qx::Error Command::process(const QStringList& commandLine) // Perform command return perform(); } + + diff --git a/app/src/command/command.h b/app/src/command/command.h index 7b2e108..79a9169 100644 --- a/app/src/command/command.h +++ b/app/src/command/command.h @@ -108,12 +108,12 @@ class Command // Standard command line option strings static inline const QString CL_OPT_HELP_S_NAME = u"h"_s; - static inline const QString CL_OPT_HELP_L_NAME = u"help"_s; static inline const QString CL_OPT_HELP_E_NAME = u"?"_s; + static inline const QString CL_OPT_HELP_L_NAME = u"help"_s; static inline const QString CL_OPT_HELP_DESC = u"Prints this help message."_s; // Standard command line options - static inline const QCommandLineOption CL_OPTION_HELP{{CL_OPT_HELP_S_NAME, CL_OPT_HELP_L_NAME, CL_OPT_HELP_E_NAME}, CL_OPT_HELP_DESC}; // Boolean option + static inline const QCommandLineOption CL_OPTION_HELP{{CL_OPT_HELP_S_NAME, CL_OPT_HELP_E_NAME, CL_OPT_HELP_L_NAME}, CL_OPT_HELP_DESC}; // Boolean option static inline const QList CL_OPTIONS_STANDARD{&CL_OPTION_HELP}; // Meta @@ -162,6 +162,8 @@ class Command virtual Qx::Error perform() = 0; public: + virtual bool requiresFlashpoint() const; + virtual bool autoBlockNewInstances() const; Qx::Error process(const QStringList& commandLine); }; diff --git a/app/src/controller.cpp b/app/src/controller.cpp index 87565be..1ab58e7 100644 --- a/app/src/controller.cpp +++ b/app/src/controller.cpp @@ -45,7 +45,9 @@ Controller::Controller(QObject* parent) : connect(driver, &Driver::message, &mStatusRelay, &StatusRelay::messageHandler, Qt::BlockingQueuedConnection); // Allows optional blocking connect(driver, &Driver::blockingErrorOccurred, &mStatusRelay, &StatusRelay::blockingErrorHandler, Qt::BlockingQueuedConnection); connect(driver, &Driver::saveFileRequested, &mStatusRelay, &StatusRelay::saveFileRequestHandler, Qt::BlockingQueuedConnection); + connect(driver, &Driver::existingDirRequested, &mStatusRelay, &StatusRelay::existingDirectoryRequestHandler, Qt::BlockingQueuedConnection); connect(driver, &Driver::itemSelectionRequested, &mStatusRelay, &StatusRelay::itemSelectionRequestHandler, Qt::BlockingQueuedConnection); + connect(driver, &Driver::questionAnswerRequested, &mStatusRelay, &StatusRelay::questionAnswerRequestHandler, Qt::BlockingQueuedConnection); // Connect quit handler connect(&mStatusRelay, &StatusRelay::quitRequested, this, &Controller::quitRequestHandler); diff --git a/app/src/frontend/statusrelay.cpp b/app/src/frontend/statusrelay.cpp index f525461..9cd24ba 100644 --- a/app/src/frontend/statusrelay.cpp +++ b/app/src/frontend/statusrelay.cpp @@ -110,6 +110,16 @@ void StatusRelay::saveFileRequestHandler(QSharedPointer file, Core::Sav qFatal("No response argument provided!"); } +void StatusRelay::existingDirectoryRequestHandler(QSharedPointer dir, Core::ExistingDirRequest request) +{ + if(dir) + { + *dir = QFileDialog::getExistingDirectory(nullptr, request.caption, request.dir, request.options); + } + else + qFatal("No response argument provided!"); +} + void StatusRelay::itemSelectionRequestHandler(QSharedPointer item, const Core::ItemSelectionRequest& request) { if(item) @@ -127,6 +137,15 @@ void StatusRelay::clipboardUpdateRequestHandler(const QString& text) mSystemClipboard->setText(text); } +void StatusRelay::questionAnswerRequestHandler(QSharedPointer response, const QString& question) +{ + + if(response) + *response = QMessageBox::question(nullptr, QString(), question) == QMessageBox::Yes; + else + qFatal("No response argument provided!"); +} + void StatusRelay::longTaskProgressHandler(quint64 progress) { mLongTaskProgressDialog.setValue(progress); diff --git a/app/src/frontend/statusrelay.h b/app/src/frontend/statusrelay.h index 84e1d2c..9894afa 100644 --- a/app/src/frontend/statusrelay.h +++ b/app/src/frontend/statusrelay.h @@ -51,8 +51,10 @@ public slots: void blockingErrorHandler(QSharedPointer response, Core::BlockingError blockingError); void messageHandler(const Message& message); void saveFileRequestHandler(QSharedPointer file, Core::SaveFileRequest request); + void existingDirectoryRequestHandler(QSharedPointer dir, Core::ExistingDirRequest request); void itemSelectionRequestHandler(QSharedPointer item, const Core::ItemSelectionRequest& request); void clipboardUpdateRequestHandler(const QString& text); + void questionAnswerRequestHandler(QSharedPointer response, const QString& question); // Long Job void longTaskProgressHandler(quint64 progress); diff --git a/app/src/kernel/buildinfo.h b/app/src/kernel/buildinfo.h new file mode 100644 index 0000000..c3664ff --- /dev/null +++ b/app/src/kernel/buildinfo.h @@ -0,0 +1,19 @@ +#ifndef BUILDINFO_H +#define BUILDINFO_H + +// Qt Includes +#include +#include + +struct BuildInfo +{ + enum System{Windows, Linux}; + enum Linkage{Static, Shared}; + + System system; + Linkage linkage; + QString compiler; + QVersionNumber compilerVersion; +}; + +#endif // BUILDINFO_H diff --git a/app/src/kernel/core.cpp b/app/src/kernel/core.cpp index b0e2a54..4f7c9b2 100644 --- a/app/src/kernel/core.cpp +++ b/app/src/kernel/core.cpp @@ -6,6 +6,7 @@ // Qx Includes #include +#include // Magic Enum Includes #include @@ -25,7 +26,7 @@ #include "task/t-awaitdocker.h" #endif #include "utility.h" -#include "project_vars.h" +#include "_buildinfo.h" //=============================================================================================================== // CoreError @@ -61,7 +62,34 @@ Core::Core(QObject* parent) : mCriticalErrorOccurred(false), mStatusHeading(u"Initializing"_s), mStatusMessage(u"..."_s) -{} +{ + establishCanonCore(*this); // Ignore return value as there should never be more than one Core with current design +} + +//-Class Functions------------------------------------------------------------------------------------------------------ +//Private: +bool Core::establishCanonCore(Core& cc) +{ + if(!smDefaultMessageHandler) + smDefaultMessageHandler = qInstallMessageHandler(qtMessageHandler); + + if(smCanonCore) + return false; + + smCanonCore = &cc; + return true; +} + +void Core::qtMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg) +{ + // Log messages + if(smCanonCore && smCanonCore->isLogOpen()) + smCanonCore->logQtMessage(type, context, msg); + + // Defer to default behavior + if(smDefaultMessageHandler) + smDefaultMessageHandler(type, context, msg); +} //-Instance Functions------------------------------------------------------------- //Private: @@ -224,6 +252,41 @@ Qx::Error Core::searchAndFilterEntity(QUuid& returnBuffer, QString name, bool ex } } +void Core::logQtMessage(QtMsgType type, const QMessageLogContext& context, const QString& msg) +{ +#if defined QT_NO_MESSAGELOGCONTEXT || !defined QT_MESSAGELOGCONTEXT + QString msgWithContext = msg; +#else + static const QString cTemplate = u"(%1:%2, %3) %4"_s; + static const QString unk = u"Unk."_s; + QString msgWithContext = cTemplate.arg( + context.file ? QString(context.file) : unk, + context.line >= 0 ? QString::number(context.line) : unk, + context.function ? QString(context.function) : unk, + msg + ); +#endif + + switch (type) + { + case QtDebugMsg: + logEvent(NAME, u"SYSTEM DEBUG) "_s + msgWithContext); + break; + case QtInfoMsg: + logEvent(NAME, u"SYSTEM INFO) "_s + msgWithContext); + break; + case QtWarningMsg: + logError(NAME, CoreError(CoreError::InternalError, msgWithContext, Qx::Warning)); + break; + case QtCriticalMsg: + logError(NAME, CoreError(CoreError::InternalError, msgWithContext, Qx::Err)); + break; + case QtFatalMsg: + logError(NAME, CoreError(CoreError::InternalError, msgWithContext, Qx::Critical)); + break; + } +} + //Public: Qx::Error Core::initialize(QStringList& commandLine) { @@ -246,7 +309,7 @@ Qx::Error Core::initialize(QStringList& commandLine) if(!globalOptions.isEmpty()) globalOptions += ' '; // Space after every switch except first one - globalOptions += u"--"_s + (*clOption).names().at(1); // Always use long name + globalOptions += u"--"_s + (*clOption).names().constLast(); // Always use long name // Add value of switch if it takes one if(!(*clOption).valueName().isEmpty()) @@ -444,6 +507,13 @@ Qx::Error Core::findAddAppIdFromName(QUuid& returnBuffer, QUuid parent, QString return searchAndFilterEntity(returnBuffer, name, exactName, parent); } +bool Core::blockNewInstances() +{ + bool b = Qx::enforceSingleInstance(SINGLE_INSTANCE_ID); + logEvent(NAME, b ? LOG_EVENT_FURTHER_INSTANCE_BLOCK_SUCC : LOG_EVENT_FURTHER_INSTANCE_BLOCK_FAIL); + return b; +} + CoreError Core::enqueueStartupTasks() { logEvent(NAME, LOG_EVENT_ENQ_START); @@ -726,6 +796,8 @@ Qx::Error Core::enqueueDataPackTasks(const Fp::GameData& gameData) void Core::enqueueSingleTask(Task* task) { mTaskQueue.push(task); logTask(NAME, task); } void Core::clearTaskQueue() { mTaskQueue = {}; } +bool Core::isLogOpen() const { return mLogger->isOpen(); } + void Core::logCommand(QString src, QString commandName) { Qx::IoOpReport logReport = mLogger->recordGeneralEvent(src, COMMAND_LABEL.arg(commandName)); @@ -839,6 +911,18 @@ QString Core::requestSaveFilePath(const SaveFileRequest& request) return *file; } +QString Core::requestExistingDirPath(const ExistingDirRequest& request) +{ + // Response holder + QSharedPointer dir = QSharedPointer::create(); + + // Emit and get response + emit existingDirRequested(dir, request); + + // Return response + return *dir; +} + QString Core::requestItemSelection(const ItemSelectionRequest& request) { // Response holder @@ -853,6 +937,24 @@ QString Core::requestItemSelection(const ItemSelectionRequest& request) void Core::requestClipboardUpdate(const QString& text) { emit clipboardUpdateRequested(text); } +bool Core::requestQuestionAnswer(const QString& question) +{ + // Show question if allowed + if(mNotificationVerbosity != NotificationVerbosity::Silent) + { + // Response holder + QSharedPointer response = QSharedPointer::create(false); + + // Emit and get response + emit questionAnswerRequested(response, question); + + // Return response + return *response; + } + else + return false; // Assume "No" +} + Fp::Install& Core::fpInstall() { return *mFlashpointInstall; } const QProcessEnvironment& Core::childTitleProcessEnvironment() { return mChildTitleProcEnv; } Core::NotificationVerbosity Core::notifcationVerbosity() const { return mNotificationVerbosity; } @@ -869,3 +971,21 @@ void Core::setStatus(QString heading, QString message) mStatusMessage = message; emit statusChanged(heading, message); } + +BuildInfo Core::buildInfo() const +{ + constexpr auto sysOpt = magic_enum::enum_cast(BUILDINFO_SYSTEM); + static_assert(sysOpt.has_value(), "Configured on unsupported system!"); + + constexpr auto linkOpt = magic_enum::enum_cast(BUILDINFO_LINKAGE); + static_assert(linkOpt.has_value(), "Invalid BuildInfo linkage string!"); + + static BuildInfo info{ + .system = sysOpt.value(), + .linkage = linkOpt.value(), + .compiler = BUILDINFO_COMPILER, + .compilerVersion = QVersionNumber::fromString(BUILDINFO_COMPILER_VER_STR) + }; + + return info; +} diff --git a/app/src/kernel/core.h b/app/src/kernel/core.h index 8c4eca3..f134043 100644 --- a/app/src/kernel/core.h +++ b/app/src/kernel/core.h @@ -23,6 +23,7 @@ // Project Includes #include "task/task.h" #include "project_vars.h" +#include "kernel/buildinfo.h" // General Aliases using ErrorCode = quint32; @@ -34,19 +35,21 @@ class QX_ERROR_TYPE(CoreError, "CoreError", 1200) public: enum Type { - NoError = 0, - InvalidOptions = 1, - TitleNotFound = 2, - TooManyResults = 3, - ConfiguredServerMissing = 4, - DataPackSumMismatch = 5, - DataPackSourceMissing = 6 + NoError, + InternalError, + InvalidOptions, + TitleNotFound, + TooManyResults, + ConfiguredServerMissing, + DataPackSumMismatch, + DataPackSourceMissing }; //-Class Variables------------------------------------------------------------- private: static inline const QHash ERR_STRINGS{ {NoError, u""_s}, + {InternalError, u"Internal system error."_s}, {InvalidOptions, u"Invalid global options provided."_s}, {TitleNotFound, u"Could not find the title in the Flashpoint database."_s}, {TooManyResults, u"More results than can be presented were returned in a search."_s}, @@ -114,6 +117,13 @@ class Core : public QObject QFileDialog::Options options; }; + struct ExistingDirRequest + { + QString caption; + QString dir; + QFileDialog::Options options = QFileDialog::ShowDirsOnly; + }; + struct ItemSelectionRequest { QString caption; @@ -121,9 +131,11 @@ class Core : public QObject QStringList items; }; - //-Class Variables------------------------------------------------------------------------------------------------------ public: + // Single Instance ID + static inline const QString SINGLE_INSTANCE_ID = u"CLIFp_ONE_INSTANCE"_s; // Basically never change this + // Status static inline const QString STATUS_DISPLAY = u"Displaying"_s; static inline const QString STATUS_DISPLAY_HELP = u"Help"_s; @@ -145,6 +157,8 @@ class Core : public QObject // Logging - Messages static inline const QString LOG_EVENT_INIT = u"Initializing CLIFp..."_s; static inline const QString LOG_EVENT_GLOBAL_OPT = u"Global Options: %1"_s; + static inline const QString LOG_EVENT_FURTHER_INSTANCE_BLOCK_SUCC = u"Successfully locked standard instance count..."_s; + static inline const QString LOG_EVENT_FURTHER_INSTANCE_BLOCK_FAIL = u"Failed to lock standard instance count"_s; static inline const QString LOG_EVENT_G_HELP_SHOWN = u"Displayed general help information"_s; static inline const QString LOG_EVENT_VER_SHOWN = u"Displayed version information"_s; static inline const QString LOG_EVENT_NOTIFCATION_LEVEL = u"Notification Level is: %1"_s; @@ -173,8 +187,8 @@ class Core : public QObject // Global command line option strings static inline const QString CL_OPT_HELP_S_NAME = u"h"_s; - static inline const QString CL_OPT_HELP_L_NAME = u"help"_s; static inline const QString CL_OPT_HELP_E_NAME = u"?"_s; + static inline const QString CL_OPT_HELP_L_NAME = u"help"_s; static inline const QString CL_OPT_HELP_DESC = u"Prints this help message."_s; static inline const QString CL_OPT_VERSION_S_NAME = u"v"_s; @@ -190,7 +204,7 @@ class Core : public QObject static inline const QString CL_OPT_SILENT_DESC = u"Silences all messages (takes precedence over quiet mode)."_s; // Global command line options - static inline const QCommandLineOption CL_OPTION_HELP{{CL_OPT_HELP_S_NAME, CL_OPT_HELP_L_NAME, CL_OPT_HELP_E_NAME}, CL_OPT_HELP_DESC}; // Boolean option + static inline const QCommandLineOption CL_OPTION_HELP{{CL_OPT_HELP_S_NAME, CL_OPT_HELP_E_NAME, CL_OPT_HELP_L_NAME}, CL_OPT_HELP_DESC}; // Boolean option static inline const QCommandLineOption CL_OPTION_VERSION{{CL_OPT_VERSION_S_NAME, CL_OPT_VERSION_L_NAME}, CL_OPT_VERSION_DESC}; // Boolean option static inline const QCommandLineOption CL_OPTION_QUIET{{CL_OPT_QUIET_S_NAME, CL_OPT_QUIET_L_NAME}, CL_OPT_QUIET_DESC}; // Boolean option static inline const QCommandLineOption CL_OPTION_SILENT{{CL_OPT_SILENT_S_NAME, CL_OPT_SILENT_L_NAME}, CL_OPT_SILENT_DESC}; // Boolean option @@ -211,7 +225,7 @@ class Core : public QObject static inline const QString HELP_COMMAND_TEMPL = u"
%1:  %2"_s; // Command line messages - static inline const QString CL_VERSION_MESSAGE = u"CLI Flashpoint version " PROJECT_VERSION_STR ", designed for use with Flashpoint Archive " PROJECT_TARGET_FP_VER_PFX_STR " series"_s; + static inline const QString CL_VERSION_MESSAGE = u"CLI Flashpoint " PROJECT_VERSION_STR ", designed for use with Flashpoint Archive " PROJECT_TARGET_FP_VER_PFX_STR " series"_s; // Input strings static inline const QString MULTI_TITLE_SEL_CAP = u"Title Disambiguation"_s; @@ -228,6 +242,10 @@ class Core : public QObject // Meta static inline const QString NAME = u"core"_s; + // Qt Message Handling + static inline constinit QtMessageHandler smDefaultMessageHandler = nullptr; + static inline QPointer smCanonCore; + //-Instance Variables------------------------------------------------------------------------------------------------------ private: // Handles @@ -250,6 +268,12 @@ class Core : public QObject public: explicit Core(QObject* parent); +//-Class Functions------------------------------------------------------------------------------------------------------ +private: + // Qt Message Handling - NOTE: Storing a static instance of core is required due to the C-function pointer interface of qInstallMessageHandler() + static bool establishCanonCore(Core& cc); + static void qtMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg); + //-Instance Functions------------------------------------------------------------------------------------------------------ private: bool isActionableOptionSet(const QCommandLineParser& clParser) const; @@ -258,6 +282,7 @@ class Core : public QObject // Helper Qx::Error searchAndFilterEntity(QUuid& returnBuffer, QString name, bool exactName, QUuid parent = QUuid()); + void logQtMessage(QtMsgType type, const QMessageLogContext& context, const QString& msg); public: // Setup @@ -270,6 +295,7 @@ class Core : public QObject Qx::Error findAddAppIdFromName(QUuid& returnBuffer, QUuid parent, QString name, bool exactName = true); // Common + bool blockNewInstances(); CoreError enqueueStartupTasks(); void enqueueShutdownTasks(); #ifdef _WIN32 @@ -280,6 +306,7 @@ class Core : public QObject void clearTaskQueue(); // TODO: See if this can be done away with, it's awkward (i.e. not fill queue in first place). Think I tried to before though. // Notifications/Logging + bool isLogOpen() const; void logCommand(QString src, QString commandName); void logCommandOptions(QString src, QString commandOptions); void logError(QString src, Qx::Error error); @@ -290,8 +317,10 @@ class Core : public QObject int postBlockingError(QString src, Qx::Error error, bool log = true, QMessageBox::StandardButtons bs = QMessageBox::Ok, QMessageBox::StandardButton def = QMessageBox::NoButton); void postMessage(const Message& msg); QString requestSaveFilePath(const SaveFileRequest& request); + QString requestExistingDirPath(const ExistingDirRequest& request); QString requestItemSelection(const ItemSelectionRequest& request); void requestClipboardUpdate(const QString& text); + bool requestQuestionAnswer(const QString& question); // Member access Fp::Install& fpInstall(); @@ -307,15 +336,20 @@ class Core : public QObject QString statusMessage(); void setStatus(QString heading, QString message); + // Other + BuildInfo buildInfo() const; + //-Signals & Slots------------------------------------------------------------------------------------------------------------ signals: void statusChanged(const QString& statusHeading, const QString& statusMessage); void errorOccurred(const Core::Error& error); void blockingErrorOccurred(QSharedPointer response, const Core::BlockingError& blockingError); void saveFileRequested(QSharedPointer file, const Core::SaveFileRequest& request); + void existingDirRequested(QSharedPointer dir, const Core::ExistingDirRequest& request); void itemSelectionRequested(QSharedPointer item, const Core::ItemSelectionRequest& request); void message(const Message& message); void clipboardUpdateRequested(const QString& text); + void questionAnswerRequested(QSharedPointer response, const QString& question); }; //-Metatype Declarations----------------------------------------------------------------------------------------- diff --git a/app/src/kernel/driver.cpp b/app/src/kernel/driver.cpp index a736259..b215947 100644 --- a/app/src/kernel/driver.cpp +++ b/app/src/kernel/driver.cpp @@ -7,6 +7,7 @@ // Project Includes #include "command/command.h" +#include "command/c-update.h" #include "task/t-exec.h" #include "utility.h" @@ -60,8 +61,10 @@ void Driver::init() connect(mCore, &Core::blockingErrorOccurred, this, &Driver::blockingErrorOccurred); connect(mCore, &Core::message, this, &Driver::message); connect(mCore, &Core::saveFileRequested, this, &Driver::saveFileRequested); + connect(mCore, &Core::existingDirRequested, this, &Driver::existingDirRequested); connect(mCore, &Core::itemSelectionRequested, this, &Driver::itemSelectionRequested); connect(mCore, &Core::clipboardUpdateRequested, this, &Driver::clipboardUpdateRequested); + connect(mCore, &Core::questionAnswerRequested, this, &Driver::questionAnswerRequested); //-Setup deferred process manager------ /* NOTE: It looks like the manager should just be a stack member of TExec that is constructed @@ -149,7 +152,19 @@ void Driver::cleanup() mCore->logEvent(NAME, LOG_EVENT_CLEANUP_FINISH); } -void Driver::finish() { emit finished(mCore->logFinish(NAME, mErrorStatus.value())); } +void Driver::finish() +{ + // Clear update cache + if(CUpdate::isUpdateCacheClearable()) + { + if(CUpdateError err = CUpdate::clearUpdateCache(); err.isValid()) + mCore->logError(NAME, err); + else + mCore->logEvent(NAME, LOG_EVENT_CLEARED_UPDATE_CACHE); + } + + emit finished(mCore->logFinish(NAME, mErrorStatus.value())); +} // Helper functions std::unique_ptr Driver::findFlashpointInstall() @@ -227,59 +242,63 @@ void Driver::drive() return; } - //-Restrict app to only one instance--------------------------------------------------- - if(!Qx::enforceSingleInstance(SINGLE_INSTANCE_ID)) - { - DriverError err(DriverError::AlreadyOpen); - mCore->postError(NAME, err); - mErrorStatus = err; - finish(); - return; - } + //-Prepare Command--------------------------------------------------------------------- + QString commandStr = mArguments.first().toLower(); - // Ensure Flashpoint Launcher isn't running - if(Qx::processIsRunning(Fp::Install::LAUNCHER_NAME)) + // Check for valid command + if(CommandError ce = Command::isRegistered(commandStr); ce.isValid()) { - DriverError err(DriverError::LauncherRunning, ERR_LAUNCHER_RUNNING_TIP); - mCore->postError(NAME, err); - mErrorStatus = err; + mCore->postError(NAME, ce); + mErrorStatus = ce; finish(); return; } - //-Find and link to Flashpoint Install---------------------------------------------------------- - std::unique_ptr flashpointInstall; - mCore->logEvent(NAME, LOG_EVENT_FLASHPOINT_SEARCH); + // Create command instance + std::unique_ptr commandProcessor = Command::acquire(commandStr, *mCore); - if(!(flashpointInstall = findFlashpointInstall())) + //-Restrict app to only one instance--------------------------------------------------- + if(commandProcessor->autoBlockNewInstances() && !mCore->blockNewInstances()) { - DriverError err(DriverError::InvalidInstall, ERR_INSTALL_INVALID_TIP); + DriverError err(DriverError::AlreadyOpen); mCore->postError(NAME, err); mErrorStatus = err; finish(); return; } - mCore->logEvent(NAME, LOG_EVENT_FLASHPOINT_LINK.arg(QDir::toNativeSeparators(flashpointInstall->fullPath()))); - // Insert into core - mCore->attachFlashpoint(std::move(flashpointInstall)); + //-Handle Flashpoint Steps---------------------------------------------------------- + if(commandProcessor->requiresFlashpoint()) + { + // Ensure Flashpoint Launcher isn't running + if(Qx::processIsRunning(Fp::Install::LAUNCHER_NAME)) + { + DriverError err(DriverError::LauncherRunning, ERR_LAUNCHER_RUNNING_TIP); + mCore->postError(NAME, err); + mErrorStatus = err; + finish(); + return; + } - //-Handle Command and Command Options---------------------------------------------------------- - QString commandStr = mArguments.first().toLower(); + // Find and link to Flashpoint Install + std::unique_ptr flashpointInstall; + mCore->logEvent(NAME, LOG_EVENT_FLASHPOINT_SEARCH); - // Check for valid command - if(CommandError ce = Command::isRegistered(commandStr); ce.isValid()) - { - mCore->postError(NAME, ce); - mErrorStatus = ce; - finish(); - return; - } + if(!(flashpointInstall = findFlashpointInstall())) + { + DriverError err(DriverError::InvalidInstall, ERR_INSTALL_INVALID_TIP); + mCore->postError(NAME, err); + mErrorStatus = err; + finish(); + return; + } + mCore->logEvent(NAME, LOG_EVENT_FLASHPOINT_LINK.arg(QDir::toNativeSeparators(flashpointInstall->fullPath()))); - // Create command instance - std::unique_ptr commandProcessor = Command::acquire(commandStr, *mCore); + // Insert into core + mCore->attachFlashpoint(std::move(flashpointInstall)); + } - // Process command + //-Process command----------------------------------------------------------------------------- mErrorStatus = commandProcessor->process(mArguments); if(mErrorStatus.isSet()) { diff --git a/app/src/kernel/driver.h b/app/src/kernel/driver.h index f57a8e3..b9b22e8 100644 --- a/app/src/kernel/driver.h +++ b/app/src/kernel/driver.h @@ -19,10 +19,10 @@ class QX_ERROR_TYPE(DriverError, "DriverError", 1201) public: enum Type { - NoError = 0, - AlreadyOpen = 1, - LauncherRunning = 2, - InvalidInstall = 3, + NoError, + AlreadyOpen, + LauncherRunning, + InvalidInstall, }; //-Class Variables------------------------------------------------------------- @@ -61,12 +61,9 @@ class Driver : public QObject Q_OBJECT //-Class Variables------------------------------------------------------------------------------------------------------ private: - // Single Instance ID - static inline const QString SINGLE_INSTANCE_ID = u"CLIFp_ONE_INSTANCE"_s; // Basically never change this - // Error Messages static inline const QString ERR_LAUNCHER_RUNNING_TIP = u"Please close the Launcher first."_s; - static inline const QString ERR_INSTALL_INVALID_TIP = u"You may need to update CLIFp."_s; + static inline const QString ERR_INSTALL_INVALID_TIP = u"You may need to update (i.e. the 'update' command)."_s; // Logging static inline const QString LOG_EVENT_FLASHPOINT_SEARCH = u"Searching for Flashpoint root..."_s; @@ -86,6 +83,7 @@ class Driver : public QObject static inline const QString LOG_EVENT_TASK_SKIP_QUIT = u"Task skipped because the application is quitting"_s; static inline const QString LOG_EVENT_QUIT_REQUEST = u"Received quit request"_s; static inline const QString LOG_EVENT_QUIT_REQUEST_REDUNDANT = u"Received redundant quit request"_s; + static inline const QString LOG_EVENT_CLEARED_UPDATE_CACHE = u"Cleared stale update cache."_s; // Meta static inline const QString NAME = u"driver"_s; @@ -154,8 +152,10 @@ public slots: void blockingErrorOccurred(QSharedPointer response, const Core::BlockingError& blockingError); void message(const Message& message); void saveFileRequested(QSharedPointer file, const Core::SaveFileRequest& request); + void existingDirRequested(QSharedPointer dir, const Core::ExistingDirRequest& request); void itemSelectionRequested(QSharedPointer item, const Core::ItemSelectionRequest& request); void clipboardUpdateRequested(const QString& text); + void questionAnswerRequested(QSharedPointer response, const QString& question); // Long task void longTaskProgressChanged(quint64 progress); diff --git a/app/src/task/t-download.cpp b/app/src/task/t-download.cpp index c6baa2b..3fadb9b 100644 --- a/app/src/task/t-download.cpp +++ b/app/src/task/t-download.cpp @@ -89,16 +89,16 @@ void TDownload::setSha256(QString sha256) { mSha256 = sha256; } void TDownload::perform() { // Setup download - QFile packFile(mDestinationPath + '/' + mDestinationFilename); - QFileInfo packFileInfo(packFile); + QFile file(mDestinationPath + '/' + mDestinationFilename); + QFileInfo fileInfo(file); Qx::DownloadTask download{ .target = mTargetFile, - .dest = packFileInfo.absoluteFilePath() + .dest = fileInfo.absoluteFilePath() }; mDownloadManager.appendTask(download); // Log/label string - QString label = LOG_EVENT_DOWNLOADING_DATA_PACK.arg(packFileInfo.fileName()); + QString label = LOG_EVENT_DOWNLOADING_FILE.arg(fileInfo.fileName()); emit eventOccurred(NAME, label); // Start download @@ -125,18 +125,21 @@ void TDownload::postDownload(Qx::DownloadManagerReport downloadReport) emit longTaskFinished(); if(downloadReport.wasSuccessful()) { - // Confirm checksum is correct - QFile packFile(mDestinationPath + '/' + mDestinationFilename); - bool checksumMatch; - Qx::IoOpReport cr = Qx::fileMatchesChecksum(checksumMatch, packFile, mSha256, QCryptographicHash::Sha256); - if(cr.isFailure() || !checksumMatch) + // Confirm checksum is correct, if supplied + if(!mSha256.isEmpty()) { - TDownloadError err(TDownloadError::ChecksumMismatch, cr.isFailure() ? cr.outcomeInfo() : u""_s); - errorStatus = err; - emit errorOccurred(NAME, errorStatus); + QFile file(mDestinationPath + '/' + mDestinationFilename); + bool checksumMatch; + Qx::IoOpReport cr = Qx::fileMatchesChecksum(checksumMatch, file, mSha256, QCryptographicHash::Sha256); + if(cr.isFailure() || !checksumMatch) + { + TDownloadError err(TDownloadError::ChecksumMismatch, cr.isFailure() ? cr.outcomeInfo() : u""_s); + errorStatus = err; + emit errorOccurred(NAME, errorStatus); + } } - else - emit eventOccurred(NAME, LOG_EVENT_DOWNLOAD_SUCC); + + emit eventOccurred(NAME, LOG_EVENT_DOWNLOAD_SUCC); } else { diff --git a/app/src/task/t-download.h b/app/src/task/t-download.h index 566bb38..ff40bb9 100644 --- a/app/src/task/t-download.h +++ b/app/src/task/t-download.h @@ -24,7 +24,7 @@ class QX_ERROR_TYPE(TDownloadError, "TDownloadError", 1252) private: static inline const QHash ERR_STRINGS{ {NoError, u""_s}, - {ChecksumMismatch, u"The title's Data Pack checksum does not match its record!"_s}, + {ChecksumMismatch, u"The file's checksum does not match its record!"_s}, {Incomeplete, u"The download could not be completed."_s} }; @@ -59,9 +59,9 @@ class TDownload : public Task static inline const QString NAME = u"TDownload"_s; // Logging - static inline const QString LOG_EVENT_DOWNLOADING_DATA_PACK = u"Downloading Data Pack %1"_s; - static inline const QString LOG_EVENT_DOWNLOAD_SUCC = u"Data Pack downloaded successfully"_s; - static inline const QString LOG_EVENT_DOWNLOAD_AUTH = u"Data Pack download unexpectedly requires authentication (%1)"_s; + static inline const QString LOG_EVENT_DOWNLOADING_FILE = u"Downloading file %1"_s; + static inline const QString LOG_EVENT_DOWNLOAD_SUCC = u"File downloaded successfully"_s; + static inline const QString LOG_EVENT_DOWNLOAD_AUTH = u"File download unexpectedly requires authentication (%1)"_s; static inline const QString LOG_EVENT_STOPPING_DOWNLOADS = u"Stopping current download(s)..."_s; //-Instance Variables------------------------------------------------------------------------------------------------ diff --git a/app/src/task/t-extract.cpp b/app/src/task/t-extract.cpp index f55975a..19c90b7 100644 --- a/app/src/task/t-extract.cpp +++ b/app/src/task/t-extract.cpp @@ -3,6 +3,7 @@ // QuaZip Includes #include +#include #include // TODO: Should probably be treated as a long task and support stopping @@ -37,100 +38,158 @@ QString TExtractError::derivePrimary() const { return ERR_STRINGS.value(mType); QString TExtractError::deriveSecondary() const { return mArchName; } QString TExtractError::deriveDetails() const { return mSpecific; } -//=============================================================================================================== -// TExtract -//=============================================================================================================== - -//-Constructor-------------------------------------------------------------------- -//Public: -TExtract::TExtract(QObject* parent) : - Task(parent) -{} +class TExtract::Extractor +{ +//-Class Variables------------------------------------------------------------------------------------------------- +private: + static inline const QString ERR_CODE_TEMPLATE = u"Code: 0x%1"_s; + +//-Instance Variables------------------------------------------------------------------------------------------------ +private: + // Data + QuaZip mZip; + QuaZipFile mCurrentZipFile; + QuaZipDir mCurrentZipDir; // NOTE: empty string for root, only supports directories currently. + QFile mCurrentDiskFile; + QDir mCurrentDiskDir; + +//-Constructor---------------------------------------------------------------------------------------------------------- +public: + Extractor(const QString& zipPath, const QString& zipDirPath, const QDir& destinationDir) : + mZip(zipPath), + mCurrentZipFile(&mZip), + mCurrentZipDir(&mZip, sanitizeZipDirPath(zipDirPath)), + mCurrentDiskDir(destinationDir) + {} + +//-Destructor---------------------------------------------------------------------------------------------------------- +public: + ~Extractor() { mZip.close(); } //-Class Functions--------------------------------------------------------------------------------------------------------------- -//Private: -TExtractError TExtract::extractZipSubFolderContentToDir(QString zipFilePath, QString subFolder, QDir dir) -{ - // Error template - QString zipName = QFileInfo(zipFilePath).fileName(); +private: + static QString sanitizeZipDirPath(const QString& zdp) + { + if(zdp.isEmpty() || zdp == '/') + return u""_s; + + QString clean = zdp; + if(clean.front() == '/') + clean = clean.sliced(1); + if(clean.back() == '/') + clean.chop(1); - // Zip file - QuaZip zipFile(zipFilePath); + return clean; + } - // Form subfolder string to match zip content scheme - if(subFolder.isEmpty()) - subFolder = '/'; +//-Instance Functions------------------------------------------------------------------------------------------------------ +private: + QString zipErrorString() const { return ERR_CODE_TEMPLATE.arg(mZip.getZipError(), 2, 16, QChar('0')); } + TExtractError makeError(TExtractError::Type t, const QString& s = {}) const { return TExtractError(mZip.getZipName(), t, s); } + TExtractError makeZipError(TExtractError::Type t) const { return TExtractError(mZip.getZipName(), t, zipErrorString()); } - if(subFolder != '/') + bool diskCdDown(const QString& subFolder) { - // Remove leading '/' if present - if(subFolder.front() == '/') - subFolder = subFolder.mid(1); + if(!mCurrentDiskDir.exists(subFolder) && !mCurrentDiskDir.mkdir(subFolder)) + return false; - // Add trailing '/' if missing - if(subFolder.back() != '/') - subFolder.append('/'); + return mCurrentDiskDir.cd(subFolder); } - // Open archive, ensure it's closed when done - if(!zipFile.open(QuaZip::mdUnzip)) - return TExtractError(zipName, TExtractError::OpenArchive); - QScopeGuard closeGuard([&](){ zipFile.close(); }); + TExtractError processDir(const QString& name = {}) + { + // Move to dir, unless processing root + if(!name.isEmpty()) + { + if(!mCurrentZipDir.cd(name)) + return makeError(TExtractError::PathError); + if(!diskCdDown(name)) + return makeError(TExtractError::MakePath); + } - // Persistent data - QuaZipFile currentArchiveFile(&zipFile); - QDir currentDirOnDisk(dir); + // Process files + const QStringList files = mCurrentZipDir.entryList(QDir::Files); + for(const QString& f : files) + processFile(f); - // Extract all files in sub-folder - for(bool atEnd = !zipFile.goToFirstFile(); !atEnd; atEnd = !zipFile.goToNextFile()) - { - QString fileName = zipFile.getCurrentFileName(); + // Process sub-folders + const QStringList subFolders = mCurrentZipDir.entryList(QDir::Dirs); + for(const QString& sf : subFolders) + processDir(sf); - // Only consider files in specified sub-folder - if(fileName.startsWith(subFolder)) + // Return to parent dir, unless root + if(!name.isEmpty()) { - // Determine path on disk - QString pathWithinFolder = fileName.mid(subFolder.size()); - QFileInfo pathOnDisk(dir.absoluteFilePath(pathWithinFolder)); - - // Update current directory and make path if necessary - if(pathOnDisk.absolutePath() != currentDirOnDisk.absolutePath()) - { - currentDirOnDisk = pathOnDisk.absoluteDir(); - if(!currentDirOnDisk.mkpath(u"."_s)) - return TExtractError(zipName, TExtractError::MakePath); - } - - // Open file in archive and read its data - if(!currentArchiveFile.open(QIODevice::ReadOnly)) - { - int zipError = zipFile.getZipError(); - return TExtractError(zipName, TExtractError::OpenArchiveFile, ERR_CODE_TEMPLATE.arg(zipError, 2, 16, QChar('0'))); - } - - QByteArray fileData = currentArchiveFile.readAll(); - currentArchiveFile.close(); - - // Open disk file and write data to it - QFile fileOnDisk(pathOnDisk.absoluteFilePath()); - if(!fileOnDisk.open(QIODevice::WriteOnly)) - return TExtractError(zipName, TExtractError::OpenDiskFile, fileOnDisk.fileName()); - - if(fileOnDisk.write(fileData) != fileData.size()) - return TExtractError(zipName, TExtractError::WriteDiskFile, fileOnDisk.fileName()); - - fileOnDisk.close(); + if(!mCurrentZipDir.cdUp() || !mCurrentDiskDir.cdUp()) + return makeError(TExtractError::PathError); } + + return TExtractError(); } - // Check if processing ended due to an error - int zipError = zipFile.getZipError(); - if(zipError != UNZ_OK) - return TExtractError(zipName, TExtractError::GeneralZip, ERR_CODE_TEMPLATE.arg(zipError, 2, 16, QChar('0'))); + TExtractError processFile(const QString name) + { + // Change to archive file + if(!mZip.setCurrentFile(mCurrentZipDir.filePath(name))) + return makeError(TExtractError::PathError); + + // Open archive file and read data + if(!mCurrentZipFile.open(QIODevice::ReadOnly)) + return makeZipError(TExtractError::OpenArchiveFile); - // Return success - return TExtractError(); -} + QByteArray fileData = mCurrentZipFile.readAll(); + mCurrentZipFile.close(); + + // Change to disk file + mCurrentDiskFile.setFileName(mCurrentDiskDir.absoluteFilePath(name)); + + // Open disk file and write data + if(!mCurrentDiskFile.open(QIODevice::WriteOnly)) + return makeError(TExtractError::OpenDiskFile, mCurrentDiskFile.errorString()); + + if(mCurrentDiskFile.write(fileData) != fileData.size()) + return makeError(TExtractError::WriteDiskFile, mCurrentDiskFile.fileName()); + + mCurrentDiskFile.close(); + + return TExtractError(); + } + +public: + TExtractError extract() + { + // Open archive + if(!mZip.open(QuaZip::mdUnzip)) + return makeZipError(TExtractError::OpenArchive); + + // Ensure zip sub-folder is valid + if(!mCurrentZipDir.exists()) + return makeError(TExtractError::InvalidSubPath); + + // Ensure root destination folder is present + if(!mCurrentDiskDir.mkpath(u"."_s)) + return makeError(TExtractError::MakePath); + + // Recurse + TExtractError err = processDir(); + + // Check for general zip error + if(!err.isValid() && mZip.getZipError() != UNZ_OK) + err = makeZipError(TExtractError::GeneralZip); + + return err; + } +}; + +//=============================================================================================================== +// TExtract +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------- +//Public: +TExtract::TExtract(QObject* parent) : + Task(parent) +{} //-Instance Functions------------------------------------------------------------- //Public: @@ -156,13 +215,11 @@ void TExtract::perform() { // Log string QFileInfo packFileInfo(mPackPath); - emit eventOccurred(NAME, LOG_EVENT_EXTRACTING_DATA_PACK.arg(packFileInfo.fileName())); + emit eventOccurred(NAME, LOG_EVENT_EXTRACTING_ARCHIVE.arg(packFileInfo.fileName())); // Extract pack - TExtractError ee = extractZipSubFolderContentToDir(mPackPath, - mPathInPack, - mDestinationPath); - + Extractor extractor(mPackPath, mPathInPack, mDestinationPath); + TExtractError ee = extractor.extract(); if(ee.isValid()) emit errorOccurred(NAME, ee); diff --git a/app/src/task/t-extract.h b/app/src/task/t-extract.h index 7597680..cc31adf 100644 --- a/app/src/task/t-extract.h +++ b/app/src/task/t-extract.h @@ -10,46 +10,52 @@ // Project Includes #include "task/task.h" +class Extractor; + class QX_ERROR_TYPE(TExtractError, "TExtractError", 1255) { friend class TExtract; - //-Class Enums------------------------------------------------------------- +//-Class Enums------------------------------------------------------------- public: enum Type { - NoError = 0, - OpenArchive = 1, - MakePath = 2, - OpenArchiveFile = 3, - OpenDiskFile = 4, - WriteDiskFile = 5, - GeneralZip = 6 + NoError, + InvalidSubPath, + OpenArchive, + MakePath, + PathError, + OpenArchiveFile, + OpenDiskFile, + WriteDiskFile, + GeneralZip }; - //-Class Variables------------------------------------------------------------- +//-Class Variables------------------------------------------------------------- private: static inline const QHash ERR_STRINGS{ {NoError, u""_s}, + {InvalidSubPath, u"Invalid path within zip."_s}, {OpenArchive, u"Failed to open archive."_s}, {MakePath, u"Failed to create file path."_s}, + {PathError, u"Unexpected deviation in path availability."_s}, {OpenArchiveFile, u"Failed to open archive file."_s}, {OpenDiskFile, u"Failed to open disk file."_s}, {WriteDiskFile, u"Failed to write disk file."_s}, {GeneralZip, u"General zip error."_s} }; - //-Instance Variables------------------------------------------------------------- +//-Instance Variables------------------------------------------------------------- private: Type mType; QString mSpecific; QString mArchName; - //-Constructor------------------------------------------------------------- +//-Constructor------------------------------------------------------------- private: TExtractError(); TExtractError(const QString& archName, Type t, const QString& s = {}); - //-Instance Functions------------------------------------------------------------- +//-Instance Functions------------------------------------------------------------- public: bool isValid() const; Type type() const; @@ -66,6 +72,8 @@ class QX_ERROR_TYPE(TExtractError, "TExtractError", 1255) class TExtract : public Task { + class Extractor; + Q_OBJECT; //-Class Variables------------------------------------------------------------------------------------------------- private: @@ -73,26 +81,19 @@ class TExtract : public Task static inline const QString NAME = u"TExtract"_s; // Logging - static inline const QString LOG_EVENT_EXTRACTING_DATA_PACK = u"Extracting Data Pack %1"_s; - - // Error - static inline const QString ERR_CODE_TEMPLATE = u"Code: 0x%1"_s; + static inline const QString LOG_EVENT_EXTRACTING_ARCHIVE = u"Extracting archive %1"_s; //-Instance Variables------------------------------------------------------------------------------------------------ private: // Data QString mPackPath; - QString mPathInPack; // NOTE: '/' for root, only supports directories currently. + QString mPathInPack; // NOTE: empty string for root, only supports directories currently. QString mDestinationPath; //-Constructor---------------------------------------------------------------------------------------------------------- public: TExtract(QObject* parent); -//-Class Functions--------------------------------------------------------------------------------------------------------------- -private: - static TExtractError extractZipSubFolderContentToDir(QString zipFilePath, QString subFolder, QDir dir); - //-Instance Functions------------------------------------------------------------------------------------------------------ public: QString name() const override; diff --git a/app/src/utility.h b/app/src/utility.h index 9fe5475..3ae94cb 100644 --- a/app/src/utility.h +++ b/app/src/utility.h @@ -13,6 +13,11 @@ #define CLIFP_PATH QCoreApplication::applicationFilePath() #define CLIFP_CUR_APP_FILENAME QFileInfo(QCoreApplication::applicationFilePath()).fileName() #define CLIFP_CUR_APP_BASENAME QFileInfo(QCoreApplication::applicationFilePath()).baseName() +#ifdef _WIN32 + #define CLIFP_CANONICAL_APP_FILNAME u"CLIFp.exe"_s +#else + #define CLIFP_CANONICAL_APP_FILNAME u"clifp"_s +#endif namespace Utility {