├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── codeql.yml │ └── main.yml ├── .gitignore ├── .sonarcloud.properties ├── .swiftlint.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── SECURITY.md ├── images ├── Custom_Settings.png ├── Destination │ ├── 1.png │ ├── 10.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png ├── Distribution │ ├── sign-notarize-1.png │ ├── sign-notarize-2.png │ ├── sign-notarize-3.png │ └── sign-notarize-4.png ├── MDMCheck │ └── mdm-check-1.png ├── PPPC.png ├── Rebranding │ ├── rebranding-1.png │ ├── rebranding-2.png │ ├── rebranding-3.png │ ├── rebranding-4.png │ ├── rebranding-5.png │ └── rebranding-6.png ├── Source │ ├── 1.png │ ├── 10.png │ ├── 11.png │ ├── 12.png │ ├── 13.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png └── readme_img.png ├── migrator.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── IBM Data Shift.xcscheme ├── migrator ├── AppContext.swift ├── AppDelegate.swift ├── Controllers │ ├── DeviceManagementHelper.swift │ ├── JamfReconManager.swift │ ├── MLogger.swift │ ├── MigrationController.swift │ └── NetworkControllers │ │ ├── NetworkBrowser.swift │ │ ├── NetworkConnection.swift │ │ └── NetworkServer.swift ├── Extensions │ ├── Array-Extension.swift │ ├── Binding-Extension.swift │ ├── Bundle-Extension.swift │ ├── Data-Extension.swift │ ├── Decodable-Extension.swift │ ├── Double-Extension.swift │ ├── Int-Extension.swift │ ├── LinearGradient-Extension.swift │ ├── NSTableView-Extension.swift │ ├── NWBrowser-Result-Extension.swift │ ├── NWParameters-Extension.swift │ ├── NWProtocolFramer-Message-Extension.swift │ ├── Notification-Extension.swift │ ├── Scene-Extension.swift │ ├── String-Extension.swift │ ├── URL-Extension.swift │ └── View-Extension.swift ├── Info.plist ├── MigratorApp.swift ├── Model │ ├── DeviceManagement │ │ ├── ConfigProfile.swift │ │ ├── ConfigProfilePayload.swift │ │ ├── ConfigProfileSection.swift │ │ ├── ConfigurationProfileDataType.swift │ │ ├── DeviceManagementState.swift │ │ └── ManagedEnvironment.swift │ ├── DuplicateFilesHandlingPolicy.swift │ ├── JamfReconMethod.swift │ ├── MigrationOption.swift │ ├── MigratorError.swift │ ├── MigratorFile.swift │ ├── MigratorFileURL.swift │ ├── MigratorPage.swift │ └── Network │ │ ├── CommunicationInterfaces.swift │ │ ├── DefaultsMessage.swift │ │ ├── FileMessage.swift │ │ ├── MigratorMessageType.swift │ │ ├── MigratorNetworkProtocol.swift │ │ ├── MigratorNetworkProtocolHeader.swift │ │ ├── NetworkDevice.swift │ │ └── SymbolicLinkMessage.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Resources │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── icon_128x128.png │ │ │ ├── icon_128x128@2x@2x.png │ │ │ ├── icon_16x16.png │ │ │ ├── icon_16x16@2x@2x.png │ │ │ ├── icon_256x256.png │ │ │ ├── icon_256x256@2x@2x.png │ │ │ ├── icon_32x32.png │ │ │ ├── icon_32x32@2x@2x.png │ │ │ ├── icon_512x512.png │ │ │ └── icon_512x512@2x@2x.png │ │ ├── Colors │ │ │ ├── Contents.json │ │ │ ├── bigButtonUnselected.colorset │ │ │ │ └── Contents.json │ │ │ ├── buttonUnselected.colorset │ │ │ │ └── Contents.json │ │ │ └── discoveryViewBackground.colorset │ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── apple_id_icon.imageset │ │ │ ├── Contents.json │ │ │ └── apple_id_logo.png │ │ ├── apps_icon.imageset │ │ │ ├── Contents.json │ │ │ └── appsIcon.pdf │ │ ├── folder_icon.imageset │ │ │ ├── Contents.json │ │ │ └── folderIcon.pdf │ │ ├── icloud_icon.imageset │ │ │ ├── Contents.json │ │ │ └── icloud.png │ │ ├── icon.imageset │ │ │ ├── Contents.json │ │ │ └── icon.png │ │ ├── new_mac.imageset │ │ │ ├── Contents.json │ │ │ └── new_mac.pdf │ │ ├── old_mac.imageset │ │ │ ├── Contents.json │ │ │ └── old_mac.pdf │ │ └── preferences_icon.imageset │ │ │ ├── Contents.json │ │ │ └── preferencesIcon.pdf │ ├── Localizable.xcstrings │ └── Utils.swift ├── Views │ ├── AppleIDView.swift │ ├── BrowserView.swift │ ├── CodeVerificationView.swift │ ├── Common │ │ ├── CodeVerificationFieldView.swift │ │ ├── DeviceListRow.swift │ │ ├── MigratorFileView.swift │ │ ├── NoBackgroundScroller.swift │ │ └── WindowAccessor.swift │ ├── FinalView.swift │ ├── JamfReconView.swift │ ├── MainView.swift │ ├── Migration │ │ ├── MigrationView.swift │ │ └── MigrationViewModel.swift │ ├── MigrationSetup │ │ ├── AdvancedSelectionView.swift │ │ ├── MigrationOptionView.swift │ │ ├── MigrationSetupView.swift │ │ └── MigrationSetupViewModel.swift │ ├── RebootView.swift │ ├── Server │ │ ├── ServerView.swift │ │ └── ServerViewModel.swift │ └── WelcomeView.swift └── migrator.entitlements ├── migratorTests └── MigratorTests.swift ├── migratorUITests ├── MigratorUITests.swift └── MigratorUITestsLaunchTests.swift └── renovate.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. macOS 11] 28 | - Project version [e.g. 1.0.0] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature Request]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | *Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots.* 2 | 3 | *List which issues are fixed by this PR. You must list at least one issue.* 4 | 5 | 6 | ## Pre-launch Checklist 7 | 8 | - [ ] I read the [Code of Conduct](/CODE_OF_CONDUCT.md) and followed the process outlined there for submitting PRs. 9 | - [ ] I listed at least one issue that this PR fixes in the description above. 10 | - [ ] I updated/added relevant documentation (doc comments with `///`). 11 | - [ ] I updated [README.md](https://github.com/IBM/mac-ibm-migration-tool/blob/main/README.md) file with new version/info - if applicable. 12 | - [ ] I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt. 13 | - [ ] All existing and new tests are passing. 14 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL Advanced" 13 | 14 | on: 15 | workflow_dispatch: 16 | # Disabled automatic runs of CodeQL scan because it doesn't work yet for Xcode 16. 17 | # push: 18 | # branches: [ "main" ] 19 | # pull_request: 20 | # branches: [ "main" ] 21 | # schedule: 22 | # - cron: '45 17 * * 0' 23 | 24 | jobs: 25 | analyze: 26 | name: Analyze 27 | # Runner size impacts CodeQL analysis time. To learn more, please see: 28 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 29 | # - https://gh.io/supported-runners-and-hardware-resources 30 | # - https://gh.io/using-larger-runners (GitHub.com only) 31 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 32 | runs-on: 'macos-15' 33 | permissions: 34 | # required for all workflows 35 | security-events: write 36 | 37 | # required to fetch internal or private CodeQL packs 38 | packages: read 39 | 40 | # only required for workflows in private repositories 41 | actions: read 42 | contents: read 43 | 44 | strategy: 45 | fail-fast: true 46 | matrix: 47 | include: 48 | - language: swift 49 | build-mode: autobuild 50 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 51 | # Use `c-cpp` to analyze code written in C, C++ or both 52 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 53 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 54 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 55 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 56 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 57 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 58 | steps: 59 | - name: Checkout repository 60 | uses: actions/checkout@v4 61 | 62 | # Initializes the CodeQL tools for scanning. 63 | - name: Initialize CodeQL 64 | uses: github/codeql-action/init@v3 65 | with: 66 | languages: swift 67 | build-mode: autobuild 68 | # If you wish to specify custom queries, you can do so here or in a config file. 69 | # By default, queries listed here will override any specified in a config file. 70 | # Prefix the list here with "+" to use these queries and those in the config file. 71 | 72 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 73 | # queries: security-extended,security-and-quality 74 | 75 | # If the analyze step fails for one of the languages you are analyzing with 76 | # "We were unable to automatically build your code", modify the matrix above 77 | # to set the build mode to "manual" for that language. Then modify this step 78 | # to build your code. 79 | # ℹ️ Command-line programs to run using the OS shell. 80 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 81 | - if: matrix.build-mode == 'manual' 82 | shell: bash 83 | run: | 84 | xcodebuild clean build -project migrator.xcodeproj -scheme 'IBM Data Shift' -destination 'platform=macOS' 85 | 86 | - name: Perform CodeQL Analysis 87 | uses: github/codeql-action/analyze@v3 88 | with: 89 | category: "/language:swift" 90 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ "main" ] 7 | pull_request: 8 | branches: [ "main", "dev" ] 9 | 10 | jobs: 11 | linting: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: GitHub Action for SwiftLint 16 | uses: norio-nomura/action-swiftlint@3.2.1 17 | test: 18 | runs-on: macos-15 19 | steps: 20 | - name: Xcode Setup 21 | uses: maxim-lobanov/setup-xcode@v1.6.0 22 | with: 23 | xcode-version: latest-stable 24 | - name: Checkout project 25 | uses: actions/checkout@v4 26 | - name: Build 27 | shell: bash 28 | run: | 29 | xcodebuild clean build -project "migrator.xcodeproj" -scheme "IBM Data Shift" -destination 'platform=macOS' | xcpretty 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## macOS 13 | **/.DS_Store 14 | 15 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 16 | build/ 17 | DerivedData/ 18 | *.moved-aside 19 | *.pbxuser 20 | !default.pbxuser 21 | *.mode1v3 22 | !default.mode1v3 23 | *.mode2v3 24 | !default.mode2v3 25 | *.perspectivev3 26 | !default.perspectivev3 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | 31 | ## App packaging 32 | *.ipa 33 | *.dSYM.zip 34 | *.dSYM 35 | 36 | ## Playgrounds 37 | timeline.xctimeline 38 | playground.xcworkspace 39 | 40 | # Swift Package Manager 41 | # 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | # Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | # *.xcodeproj 47 | # 48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 49 | # hence it is not needed unless you have added a package configuration file to your project 50 | # .swiftpm 51 | 52 | .build/ 53 | 54 | # CocoaPods 55 | # 56 | # We recommend against adding the Pods directory to your .gitignore. However 57 | # you should judge for yourself, the pros and cons are mentioned at: 58 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 59 | # 60 | # Pods/ 61 | # 62 | # Add this line if you want to avoid checking in source code from the Xcode workspace 63 | # *.xcworkspace 64 | 65 | # Carthage 66 | # 67 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 68 | # Carthage/Checkouts 69 | 70 | Carthage/Build/ 71 | 72 | # Accio dependency management 73 | Dependencies/ 74 | .accio/ 75 | 76 | # fastlane 77 | # 78 | # It is recommended to not store the screenshots in the git repo. 79 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 80 | # For more information about the recommended setup visit: 81 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 82 | 83 | fastlane/report.xml 84 | fastlane/Preview.html 85 | fastlane/screenshots/**/*.png 86 | fastlane/test_output 87 | 88 | # Code Injection 89 | # 90 | # After new code Injection tools there's a generated folder /iOSInjectionProject 91 | # https://github.com/johnno1962/injectionforxcode 92 | 93 | iOSInjectionProject/ 94 | -------------------------------------------------------------------------------- /.sonarcloud.properties: -------------------------------------------------------------------------------- 1 | # Project Version to be used as Code Definition 2 | sonar.projectVersion=1.1.0.100 3 | # Folders excluded from the scan 4 | sonar.exclusions=**/migratorTests/**,**/migratorUITests/** 5 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | disabled_rules: 3 | - line_length 4 | - compiler_protocol_init 5 | - cyclomatic_complexity 6 | - notification_center_detachment 7 | - trailing_whitespace 8 | - colon 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | the contact informations available on the MAINTAINERS.md resource. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing In General 2 | Our project welcomes external contributions. If you have an itch, please feel 3 | free to scratch it. 4 | 5 | To contribute code or documentation, please submit a [pull request](https://github.com/ibm/mac-ibm-migration-tool/pulls). 6 | 7 | A good way to familiarize yourself with the codebase and contribution process is 8 | to look for and tackle low-hanging fruit in the [issue tracker](https://github.com/ibm/mac-ibm-migration-tool/issues). 9 | Before embarking on a more ambitious contribution, please quickly [get in touch](#communication) with us. 10 | 11 | **Note: We appreciate your effort, and want to avoid a situation where a contribution 12 | requires extensive rework (by you or by us), sits in backlog for a long time, or 13 | cannot be accepted at all!** 14 | 15 | ### Proposing new features 16 | 17 | If you would like to implement a new feature, please [raise an issue](https://github.com/ibm/mac-ibm-migration-tool/issues) 18 | before sending a pull request so the feature can be discussed. This is to avoid 19 | you wasting your valuable time working on a feature that the project developers 20 | are not interested in accepting into the code base. 21 | 22 | ### Fixing bugs 23 | 24 | If you would like to fix a bug, please [raise an issue](https://github.com/ibm/mac-ibm-migration-tool/issues) before sending a 25 | pull request so it can be tracked. 26 | 27 | ### Merge approval 28 | 29 | The project maintainers use LGTM (Looks Good To Me) in comments on the code 30 | review to indicate acceptance. A change requires LGTMs from two of the 31 | maintainers of each component affected. 32 | 33 | For a list of the maintainers, see the [MAINTAINERS.md](MAINTAINERS.md) page. 34 | 35 | ## Legal 36 | 37 | Each source file must include a license header for the Apache 38 | Software License 2.0. Using the SPDX format is the simplest approach. 39 | e.g. 40 | 41 | ```text 42 | // 43 | // © Copyright IBM Corp. 2023, 2024 44 | // SPDX-License-Identifier: Apache2.0 45 | // 46 | ``` 47 | 48 | We have tried to make it as easy as possible to make contributions. This 49 | applies to how we handle the legal aspects of contribution. We use the 50 | same approach - the [Developer's Certificate of Origin 1.1 (DCO)](https://github.com/hyperledger/fabric/blob/master/docs/source/DCO1.1.txt) - that the Linux® Kernel [community](https://elinux.org/Developer_Certificate_Of_Origin) 51 | uses to manage code contributions. 52 | 53 | We simply ask that when submitting a patch for review, the developer 54 | must include a sign-off statement in the commit message. 55 | 56 | Here is an example Signed-off-by line, which indicates that the 57 | submitter accepts the DCO: 58 | 59 | ``` 60 | Signed-off-by: John Doe 61 | ``` 62 | 63 | You can include this automatically when you commit a change to your 64 | local git repository using the following command: 65 | 66 | ``` 67 | git commit -s 68 | ``` 69 | 70 | ## Communication 71 | Please feel free to connect with us by mail, see the [MAINTAINERS.md](MAINTAINERS.md) page. 72 | 73 | ## Testing 74 | Make sure that every Xcode Unit and UI test pass befor submitting any changes. 75 | 76 | ## Coding style guidelines 77 | Please follow the SwiftLintFramework rules. 78 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # MAINTAINERS 2 | 3 | Simone Martorelli - simone.martorelli@ibm.com 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IBM Data Shift 2 | 3 | ![License](https://img.shields.io/badge/license-Apache%202-1984E5) 4 | 5 | ![Swift version](https://img.shields.io/badge/swift-5.9.0-1984E5?logo=swift) 6 | ![Project version](https://img.shields.io/badge/version-1.1.0-1984E5) 7 | ![macOS](https://img.shields.io/badge/macOS-12+-bright%20green) 8 | 9 | ## Scope 10 | 11 | The purpose of this project is to provide an useful macOS Utility to facilitate the mass migration of files/apps/preferences between devices. 12 | 13 | ## Usage 14 | 15 | To contribute to this repo please see [CONTRIBUTING.md](CONTRIBUTING.md) 16 | 17 | For detailed usage of this tool please refer to the [official wiki](https://github.com/IBM/mac-ibm-migration-tool/wiki) 18 | 19 | ## Notes 20 | 21 | If you have any questions or issues you can create a new issue [here](https://github.com/IBM/mac-ibm-migration-tool/issues/new/choose). 22 | 23 | You can also find a list of the maintainers of this repo [here](MAINTAINERS.md). 24 | 25 | Don't forget to join the community in the `#mac-ibm-open-source` channel in the [MacAdmins Slack Workspace](https://www.macadmins.org). 26 | 27 | Pull requests are very welcome! Make sure your patches are well tested. 28 | Ideally create a topic branch for every separate change you make. For 29 | example: 30 | 31 | 1. Fork the repo 32 | 2. Create your feature branch (`git checkout -b my-new-feature`) 33 | 3. Commit your changes (`git commit -am 'Added some feature'`) 34 | 4. Push to the branch (`git push origin my-new-feature`) 35 | 5. Create new Pull Request 36 | 37 | ## License 38 | 39 | All source files must include a Copyright and License header. The SPDX license header is 40 | preferred because it can be easily scanned. 41 | 42 | If you would like to see the detailed LICENSE click [here](LICENSE). 43 | 44 | ```text 45 | // 46 | // © Copyright IBM Corp. 2023, 2024 47 | // SPDX-License-Identifier: Apache2.0 48 | // 49 | ``` 50 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ---------- | -------------------------- | 7 | | 1.1.0 | :white_check_mark: | 8 | | 3.0.0 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | To report a vulnerability, please e-mail to [maintainers](/MAINTAINERS.md) of this project with a description of the issue, 13 | the steps you took to create the issue, affected versions, and if known, mitigations for the issue. 14 | 15 | We should reply within three working days, probably much sooner. 16 | 17 | We use GitHub's security advisory feature to track open security issues. You should expect 18 | a close collaboration as we work to resolve the issue you have reported. 19 | 20 | You may also reach out to the team via [Github Discussions](https://github.com/IBM/mac-ibm-migration-tool/discussions) 21 | -------------------------------------------------------------------------------- /images/Custom_Settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Custom_Settings.png -------------------------------------------------------------------------------- /images/Destination/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Destination/1.png -------------------------------------------------------------------------------- /images/Destination/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Destination/10.png -------------------------------------------------------------------------------- /images/Destination/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Destination/2.png -------------------------------------------------------------------------------- /images/Destination/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Destination/3.png -------------------------------------------------------------------------------- /images/Destination/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Destination/4.png -------------------------------------------------------------------------------- /images/Destination/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Destination/5.png -------------------------------------------------------------------------------- /images/Destination/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Destination/6.png -------------------------------------------------------------------------------- /images/Destination/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Destination/7.png -------------------------------------------------------------------------------- /images/Destination/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Destination/8.png -------------------------------------------------------------------------------- /images/Destination/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Destination/9.png -------------------------------------------------------------------------------- /images/Distribution/sign-notarize-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Distribution/sign-notarize-1.png -------------------------------------------------------------------------------- /images/Distribution/sign-notarize-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Distribution/sign-notarize-2.png -------------------------------------------------------------------------------- /images/Distribution/sign-notarize-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Distribution/sign-notarize-3.png -------------------------------------------------------------------------------- /images/Distribution/sign-notarize-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Distribution/sign-notarize-4.png -------------------------------------------------------------------------------- /images/MDMCheck/mdm-check-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/MDMCheck/mdm-check-1.png -------------------------------------------------------------------------------- /images/PPPC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/PPPC.png -------------------------------------------------------------------------------- /images/Rebranding/rebranding-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Rebranding/rebranding-1.png -------------------------------------------------------------------------------- /images/Rebranding/rebranding-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Rebranding/rebranding-2.png -------------------------------------------------------------------------------- /images/Rebranding/rebranding-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Rebranding/rebranding-3.png -------------------------------------------------------------------------------- /images/Rebranding/rebranding-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Rebranding/rebranding-4.png -------------------------------------------------------------------------------- /images/Rebranding/rebranding-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Rebranding/rebranding-5.png -------------------------------------------------------------------------------- /images/Rebranding/rebranding-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Rebranding/rebranding-6.png -------------------------------------------------------------------------------- /images/Source/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Source/1.png -------------------------------------------------------------------------------- /images/Source/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Source/10.png -------------------------------------------------------------------------------- /images/Source/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Source/11.png -------------------------------------------------------------------------------- /images/Source/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Source/12.png -------------------------------------------------------------------------------- /images/Source/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Source/13.png -------------------------------------------------------------------------------- /images/Source/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Source/2.png -------------------------------------------------------------------------------- /images/Source/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Source/3.png -------------------------------------------------------------------------------- /images/Source/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Source/4.png -------------------------------------------------------------------------------- /images/Source/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Source/5.png -------------------------------------------------------------------------------- /images/Source/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Source/6.png -------------------------------------------------------------------------------- /images/Source/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Source/7.png -------------------------------------------------------------------------------- /images/Source/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Source/8.png -------------------------------------------------------------------------------- /images/Source/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/Source/9.png -------------------------------------------------------------------------------- /images/readme_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/images/readme_img.png -------------------------------------------------------------------------------- /migrator.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /migrator.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /migrator.xcodeproj/xcshareddata/xcschemes/IBM Data Shift.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 35 | 41 | 42 | 43 | 46 | 52 | 53 | 54 | 55 | 56 | 66 | 68 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /migrator/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 08/08/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { 13 | 14 | // MARK: - Published variables 15 | 16 | /// Used to track user requests to quit the app. 17 | @Published var userRequestToQuit: Bool = false 18 | 19 | // MARK: - Variables 20 | 21 | var isDeviceConnectedToPower: Bool = false { 22 | didSet { 23 | NotificationCenter.default.post(name: .devicePowerStatusChanged, object: nil, userInfo: ["newValue" : isDeviceConnectedToPower]) 24 | } 25 | } 26 | 27 | // MARK: - Private Variables 28 | 29 | private var timer: Timer? 30 | 31 | // MARK: - Instance Functions 32 | 33 | func applicationWillFinishLaunching(_ notification: Notification) { 34 | #if !DEBUG 35 | // Prevent the execution of multiple instances of the app. 36 | guard NSWorkspace.shared.runningApplications.filter({ $0.bundleIdentifier == Bundle.main.bundleIdentifier }).count < 2 else { exit(0) } 37 | #endif 38 | // Disables the automatic tabbing feature in macOS, providing more control over window management. 39 | NSWindow.allowsAutomaticWindowTabbing = false 40 | isDeviceConnectedToPower = IOPSCopyExternalPowerAdapterDetails()?.takeRetainedValue() != nil 41 | timer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(checkPowerStatus), userInfo: nil, repeats: true) 42 | #if DEBUG 43 | Utils.preventSleep() 44 | #endif 45 | Task { @MainActor in 46 | if let mainMenu = NSApplication.shared.mainMenu, 47 | let appMenu = mainMenu.items.first(where: { $0.title == Bundle.main.name }), 48 | let aboutItem = appMenu.submenu?.items.first?.copy() as? NSMenuItem { 49 | // let helpMenu = mainMenu.items.last { 50 | let firstMenu = NSMenuItem(title: Bundle.main.name, action: nil, keyEquivalent: "") 51 | firstMenu.submenu = NSMenu(title: Bundle.main.name) 52 | firstMenu.submenu?.addItem(aboutItem) 53 | mainMenu.items.removeAll() 54 | mainMenu.items.append(firstMenu) 55 | // menu.items.append(helpMenu) 56 | } 57 | } 58 | } 59 | 60 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 61 | // Returns true to quit the app when the last window is closed. 62 | return true 63 | } 64 | 65 | func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 66 | return false 67 | } 68 | 69 | // MARK: - Functions 70 | 71 | /// Runs final operations and quit the app 72 | func quit() { 73 | // Cleaning temporary settings sent by the source device. 74 | UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!) 75 | UserDefaults.standard.synchronize() 76 | exit(0) 77 | } 78 | 79 | // MARK: - Private Functions 80 | 81 | /// Periodically check if the device is connected to the power adaptor. 82 | @objc 83 | private func checkPowerStatus() { 84 | let newValue = IOPSCopyExternalPowerAdapterDetails()?.takeRetainedValue() != nil 85 | if newValue != isDeviceConnectedToPower { 86 | isDeviceConnectedToPower = newValue 87 | } 88 | } 89 | } 90 | 91 | // MARK: - NSWindowDelegate methods implementation 92 | 93 | extension AppDelegate: NSWindowDelegate { 94 | func windowShouldClose(_ sender: NSWindow) -> Bool { 95 | Task { @MainActor in 96 | self.userRequestToQuit = true 97 | } 98 | return false 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /migrator/Controllers/DeviceManagementHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceManagementHelper.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 04/09/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// Helper class used to run the device management state discovery. 13 | class DeviceManagementHelper { 14 | 15 | // MARK: - Static Constants 16 | 17 | static let shared = DeviceManagementHelper() 18 | 19 | // MARK: - Variables 20 | 21 | var state: DeviceManagementState 22 | 23 | /// Determine if possible to run Jamf Inventory Update based on the current device MDM environment state. 24 | var isJamfReconAvailable: Bool { 25 | switch state { 26 | case .managed: 27 | return true && !AppContext.shouldSkipJamfRecon 28 | default: 29 | return false 30 | } 31 | } 32 | 33 | // MARK: - Initializers 34 | 35 | init() { 36 | let command = "system_profiler -json SPConfigurationProfileDataType" 37 | var profiles: [ConfigProfile] = [] 38 | let task = Process() 39 | let pipe = Pipe() 40 | 41 | task.standardOutput = pipe 42 | task.standardError = pipe 43 | task.arguments = ["-c", command] 44 | task.launchPath = "/bin/zsh" 45 | task.standardInput = nil 46 | task.launch() 47 | 48 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 49 | let output = String(data: data, encoding: .utf8)! 50 | do { 51 | let dataType = try ConfigurationProfileDataType(from: output.replacingOccurrences(of: "\n", with: "")) 52 | profiles = dataType.sections.first(where: { $0.name == "spconfigprofile_section_deviceconfigprofiles" })?.profiles ?? [] 53 | guard !profiles.isEmpty else { 54 | state = .unmanaged 55 | return 56 | } 57 | if let managementProfile = profiles.first(where: { $0.payloads?.contains(where: { $0.name == "com.apple.mdm" }) ?? false }) { 58 | if let serverURL = managementProfile.payloads?.first(where: { $0.name == "com.apple.mdm" })?.serverURL, 59 | let environment = AppContext.mdmEnvironments.first(where: { $0.serverURL == serverURL }) { 60 | state = .managed(env: environment) 61 | } else { 62 | state = .managedByUnknownOrg 63 | } 64 | } else { 65 | state = .unmanaged 66 | } 67 | } catch { 68 | state = .unknown 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /migrator/Controllers/JamfReconManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JamfReconManager.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 04/09/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// The JamfReconManager actor is designed to manage and monitor the execution of a specific Jamf policy, 13 | /// particularly related to inventory updates (recon), using AppleScript to interact with system processes. 14 | actor JamfReconManager { 15 | 16 | // MARK: - Constants 17 | 18 | /// Logger instance for logging events and messages 19 | let logger = MLogger.main 20 | 21 | // MARK: - Computed Variables 22 | 23 | /// The ID of the Jamf policy used for the recon (inventory update) 24 | var reconPolicyID: String { 25 | switch DeviceManagementHelper.shared.state { 26 | case .managed(env: let env): 27 | return env.reconPolicyID 28 | default: 29 | return "" 30 | } 31 | } 32 | 33 | /// Checks if the Self Service application is currently running 34 | var isSelfServiceRunning: Bool { 35 | if let appleEventDescriptor = NSAppleScript(source: "do shell script \"ps aux | grep '[S]elf Service'\"")?.executeAndReturnError(nil), 36 | let output = appleEventDescriptor.stringValue { 37 | return output.contains("Self Service") 38 | } 39 | return false 40 | } 41 | 42 | /// Checks if the Jamf recon (inventory update) process is currently running 43 | var isReconRunning: Bool { 44 | if let appleEventDescriptor = NSAppleScript(source: "do shell script \"ps aux | grep '[j]amf'\"")?.executeAndReturnError(nil), 45 | let output = appleEventDescriptor.stringValue { 46 | return output.contains("jamf recon") 47 | } 48 | return false 49 | } 50 | 51 | /// Checks if any Jamf policies are currently running 52 | var areJamfPoliciesRunning: Bool { 53 | if let appleEventDescriptor = NSAppleScript(source: "do shell script \"ps aux | grep '[j]amf'\"")?.executeAndReturnError(nil), 54 | let output = appleEventDescriptor.stringValue { 55 | return output.contains("jamf policy") 56 | } 57 | return false 58 | } 59 | 60 | // MARK: - Public Methods 61 | 62 | /// Silently launches the Self Service application without bringing it to the foreground 63 | func silentlyRunSelfService() { 64 | logger.log("jamfReconManager.runJamfRecon: Silently starting Self Service.", type: .default) 65 | _ = NSAppleScript(source: "do shell script \"/usr/bin/open -j '\(AppContext.storePath)'\"")?.executeAndReturnError(nil) 66 | } 67 | 68 | /// Asynchronously queues and starts the Jamf recon policy, then waits for it to begin running 69 | func queueReconPolicy() async { 70 | logger.log("jamfReconManager.runJamfRecon: Running Jamf Inventory Update policy in the queue.", type: .default) 71 | _ = NSAppleScript(source: "do shell script \"/usr/bin/open -g 'jamfselfservice://content?entity=policy&id=\(reconPolicyID)&action=execute'\"")?.executeAndReturnError(nil) 72 | await withCheckedContinuation { continuation in 73 | Task.detached(priority: .utility) { 74 | sleep(10) 75 | while await !self.isReconRunning { 76 | sleep(2) 77 | } 78 | continuation.resume() 79 | } 80 | } 81 | } 82 | 83 | /// Immediately runs the Jamf recon policy without waiting for its execution 84 | func runReconPolicy() { 85 | logger.log("jamfReconManager.runJamfRecon: Running Jamf Inventory Update policy.", type: .default) 86 | _ = NSAppleScript(source: "do shell script \"/usr/bin/open -g 'jamfselfservice://content?entity=policy&id=\(reconPolicyID)&action=execute'\"")?.executeAndReturnError(nil) 87 | } 88 | 89 | /// Asynchronously waits for the completion of the Jamf recon process 90 | func waitForReconCompletion() async { 91 | logger.log("jamfReconManager.runJamfRecon: Tracking the completion of the Jamf Inventory Update.", type: .default) 92 | await withCheckedContinuation { continuation in 93 | Task.detached(priority: .utility) { 94 | sleep(10) 95 | while await self.isReconRunning { 96 | sleep(2) 97 | } 98 | continuation.resume() 99 | } 100 | } 101 | } 102 | 103 | /// Kills the Self Service application if it is currently running 104 | func killSelfService() { 105 | logger.log("jamfReconManager.runJamfRecon: Closing Self Service.", type: .default) 106 | _ = NSAppleScript(source: "do shell script \"killall 'Self Service'\"")?.executeAndReturnError(nil) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /migrator/Controllers/MLogger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MLogger.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 21/03/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | import os.log 12 | 13 | /// Object who handle the logging process for the app. 14 | final class MLogger { 15 | 16 | enum LogLevel: String { 17 | case noLog 18 | case standard 19 | case debug 20 | } 21 | 22 | // MARK: - Static Variables 23 | 24 | /// Shared instance of MLogger. 25 | static let main: MLogger = MLogger() 26 | 27 | // MARK: - Private Variables 28 | 29 | /// System logger. 30 | private var logger: Logger 31 | /// Level of verbosity for the logs. 32 | private var logLevel: LogLevel 33 | 34 | private var logFileHandle: FileHandle? 35 | 36 | // MARK: - Intializers 37 | 38 | private init() { 39 | self.logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Migration") 40 | self.logLevel = AppContext.loggingLevel 41 | if let logFileURL = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.appendingPathComponent("Logs").appendingPathComponent("\(Bundle.main.name).log") { 42 | if !FileManager.default.fileExists(atPath: logFileURL.relativePath) { 43 | FileManager.default.createFile(atPath: logFileURL.relativePath, contents: nil) 44 | } 45 | self.logFileHandle = try? FileHandle(forWritingTo: logFileURL) 46 | _ = try? logFileHandle?.seekToEnd() 47 | } 48 | } 49 | 50 | deinit { 51 | try? logFileHandle?.close() 52 | } 53 | 54 | // MARK: Public Methods 55 | 56 | /// Log the given message usign the given log level. 57 | /// - Parameters: 58 | /// - message: the log message. 59 | /// - type: the log level. 60 | func log(_ message: String, type: OSLogType = .debug) { 61 | switch logLevel { 62 | case .noLog: 63 | break 64 | case .standard: 65 | switch type { 66 | case .error, .fault, .default: 67 | logger.log(level: type, "\(message)") 68 | write(message, type: type) 69 | default: 70 | logger.log(level: type, "\(message)") 71 | } 72 | case .debug: 73 | logger.log(level: type, "\(message)") 74 | write(message, type: type) 75 | } 76 | } 77 | 78 | // MARK: - Private Methods 79 | 80 | /// Write the logs to the log file located in ~/Library/Logs/ 81 | private func write(_ message: String, type: OSLogType) { 82 | if let data = "[\(Date().formatted(date: .abbreviated, time: .complete))] \(type.humanReadableValue.capitalized): \(message)\n".data(using: .utf8) { 83 | try? logFileHandle?.write(contentsOf: data) 84 | } 85 | } 86 | } 87 | 88 | extension OSLogType { 89 | /// Human readable value for the different OSLogType(s). 90 | var humanReadableValue: String { 91 | switch self { 92 | case .debug: 93 | return "debug" 94 | case .info: 95 | return "info" 96 | case .default: 97 | return "info" 98 | case .error: 99 | return "error" 100 | case .fault: 101 | return "fault" 102 | default: 103 | return "info" 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /migrator/Controllers/NetworkControllers/NetworkBrowser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkBrowser.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 15/11/2023. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | import Network 12 | import Combine 13 | 14 | /// Manages the discovery of network services using NWBrowser. It provides functionality to start and stop the discovery process and publishes updates on the browser's state and discovered services. 15 | final class NetworkBrowser { 16 | 17 | // MARK: - Published Properties 18 | 19 | /// Publishes updates on the state of the NWBrowser instance. 20 | let onNewBrowserState = PassthroughSubject() 21 | /// Publishes changes in the discovered network services. 22 | let onNewBrowserResults = PassthroughSubject, Never>() 23 | 24 | // MARK: - Private Variables 25 | 26 | /// The NWBrowser instance used for discovering network services. 27 | private var browser: NWBrowser 28 | 29 | // MARK: - Private Constants 30 | 31 | /// Logger instance. 32 | private let logger: MLogger = MLogger.main 33 | 34 | // MARK: - Initializer 35 | 36 | /// Initializes a new NetworkBrowser instance configured for discovering Bonjour services. 37 | init() { 38 | let parameters = NWParameters() 39 | parameters.includePeerToPeer = true 40 | // Configures the browser for discovering services with the specified Bonjour type and domain. 41 | browser = NWBrowser(for: .bonjour(type: AppContext.networkServiceIdentifier, domain: nil), using: parameters) 42 | } 43 | 44 | // MARK: - Public Methods 45 | 46 | /// Starts the discovery process of network services. 47 | func start() { 48 | // Sets up handlers to publish state updates and discovered services changes. 49 | browser.stateUpdateHandler = { [weak self] newState in 50 | self?.onNewBrowserState.send(newState) 51 | } 52 | browser.browseResultsChangedHandler = { [weak self] _, changes in 53 | self?.onNewBrowserResults.send(changes) 54 | } 55 | // Starts the NWBrowser instance on the main queue. 56 | browser.start(queue: .main) 57 | } 58 | 59 | /// Stops the discovery process and resets the NWBrowser instance. 60 | func stop() { 61 | // Cancels the current browsing session. 62 | browser.cancel() 63 | // Reinitializes the browser to be ready for a new discovery session. 64 | let parameters = NWParameters() 65 | parameters.includePeerToPeer = true 66 | browser = NWBrowser(for: .bonjour(type: "_migrator._tcp", domain: nil), using: parameters) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /migrator/Controllers/NetworkControllers/NetworkServer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkServer.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 14/11/2023. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Network 11 | import Combine 12 | 13 | /// Manages a network listener for discovering and accepting connections from network clients. 14 | /// This class specifically listens for Bonjour services of type "_migrator._tcp". 15 | final class NetworkServer { 16 | 17 | // MARK: - Published Properties 18 | 19 | /// Publishes new connections established with the network service. 20 | let onNewConnection = PassthroughSubject() 21 | /// Publishes updates on the listener's state to track its lifecycle and status. 22 | let onNewListenerState = PassthroughSubject() 23 | 24 | // MARK: - Private Variables 25 | 26 | /// The network listener responsible for accepting incoming network connections. 27 | private var listener: NWListener? 28 | 29 | // MARK: - Private Constants 30 | 31 | /// Logger instance. 32 | private let logger: MLogger = MLogger.main 33 | 34 | // MARK: - Lifecycle Methods 35 | 36 | /// Starts the network listener to make the device discoverable as a service of type "_migrator._tcp". 37 | /// The listener will only accept connections that provide a matching passcode. 38 | /// - Parameter passcode: A passcode required for clients to connect to this service. 39 | func start(withPasscode passcode: String) throws { 40 | let parameters = NWParameters(passcode: passcode) 41 | parameters.attribution = .developer 42 | listener = try NWListener(using: parameters) 43 | listener?.service = NWListener.Service(type: AppContext.networkServiceIdentifier) 44 | // Handler for listener state updates. 45 | listener?.stateUpdateHandler = { [weak self] newState in 46 | self?.onNewListenerState.send(newState) 47 | } 48 | 49 | // Handler for establishing new connections. 50 | listener?.newConnectionHandler = { [weak self] newConnection in 51 | self?.onNewConnection.send(newConnection) 52 | } 53 | 54 | listener?.start(queue: .main) 55 | } 56 | 57 | /// Stops the currently running network listener. 58 | func stop() { 59 | listener?.cancel() 60 | listener = nil 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /migrator/Extensions/Array-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array-Extension.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 06/05/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | // swiftlint:disable force_cast 13 | extension Array { 14 | mutating func checkAndAppendFile(_ file: MigratorFile) { 15 | guard file.type != .socket else { return } 16 | self.append(file as! Element) 17 | } 18 | } 19 | // swiftlint:enable force_cast 20 | -------------------------------------------------------------------------------- /migrator/Extensions/Binding-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Binding-Extension.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 31/01/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | extension Binding { 13 | /// When the `Binding`'s `wrappedValue` changes, the given closure is executed. 14 | /// - Parameter closure: Chunk of code to execute whenever the value changes. 15 | /// - Returns: New `Binding`. 16 | func onUpdate(_ closure: @escaping () -> Void) -> Binding { 17 | Binding(get: { 18 | wrappedValue 19 | }, set: { newValue in 20 | wrappedValue = newValue 21 | closure() 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /migrator/Extensions/Bundle-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle-Extension.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 14/08/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | extension Bundle { 13 | var name: String { 14 | return Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "app.name.backup".localized 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /migrator/Extensions/Data-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data-Extension.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 15/01/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | extension Data { 13 | /// Extracts a `Codable` object from a specified range of the data and then removes that range from the data. 14 | /// - Parameters: 15 | /// - range: The range within the data from which to extract the object. 16 | /// - type: The type of the object to extract. Must conform to `Codable`. 17 | /// - Throws: `MigratorError.fileError` if the specified range is not valid within the data. 18 | /// - Returns: An instance of the specified `Codable` type. 19 | mutating func extractObject(from range: Range, ofType type: T.Type) throws -> T { 20 | guard self.count >= range.upperBound else { 21 | throw MigratorError.fileError(type: .noInfo) 22 | } 23 | let infoData = self.subdata(in: range) 24 | self.removeSubrange(range) 25 | 26 | return try JSONDecoder().decode(T.self, from: infoData) 27 | } 28 | 29 | /// Encodes a `Codable` object and prepends it to the data. 30 | /// - Parameter object: The `Codable` object to include. 31 | /// - Throws: An error if the object cannot be encoded. 32 | /// - Returns: The byte count of the encoded object. 33 | mutating func include(object: T) throws -> Int { 34 | let encodedObject = try JSONEncoder().encode(object) 35 | self.insert(contentsOf: encodedObject, at: 0) 36 | return encodedObject.count 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /migrator/Extensions/Decodable-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Decodable-Extension.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 26/04/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | extension Decodable { 13 | /// Initializing object decoding JSON format string. 14 | /// - Parameter json: the JSON format string. 15 | /// - Throws: deconding or data errors. 16 | init(from json: String) throws { 17 | guard let jsonData = json.data(using: .utf8) else { 18 | throw MigratorError.internalError(type: .data) 19 | } 20 | do { 21 | self = try JSONDecoder().decode(Self.self, from: jsonData) 22 | } catch { 23 | throw MigratorError.internalError(type: .data) 24 | } 25 | } 26 | /// Intializing obeject from decoding JSON file. 27 | /// - Parameter url: JSON file url. 28 | /// - Throws: deconding or url errors. 29 | init(from url: URL) throws { 30 | guard let jsonData = try? Data(contentsOf: url, options: .mappedIfSafe) else { 31 | throw MigratorError.internalError(type: .data) 32 | } 33 | do { 34 | self = try JSONDecoder().decode(Self.self, from: jsonData) 35 | } catch { 36 | throw MigratorError.internalError(type: .data) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /migrator/Extensions/Double-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double-Extension.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 09/09/2024. 6 | // Copyright © 2024 IBM. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Double { 12 | /// Generate a pretty formatted string that describe the time left starting from seconds. 13 | /// - Parameter seconds: seconds left. 14 | /// - Returns: pretty formatted string describing the time left. e.g. ~ 3 hours and 5 minutes 15 | func prettyFormattedTimeLeft() -> String { 16 | let seconds = Int(self) 17 | let components = (seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60) 18 | if components.0 == 0 { 19 | if components.1 == 0 { 20 | return "Less than a minute" 21 | } 22 | return "~ \(components.1 > 0 ? "\(components.1) minute\(components.1 == 1 ? "" : "s")" : "")" 23 | } else { 24 | return "~ \(components.0) hour\(components.0 == 1 ? "" : "s")" + (components.1 > 0 ? " and \(components.1) minute\(components.1 == 1 ? "" : "s")" : "") 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /migrator/Extensions/Int-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Int-Extension.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 14/02/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | extension Int { 13 | /// Return a string representing a formatted file size using the integers as bytes count. 14 | var fileSizeToFormattedString: String { 15 | let byteCountFormatter = ByteCountFormatter() 16 | byteCountFormatter.countStyle = .file 17 | return byteCountFormatter.string(for: self) ?? "Unknown Size" 18 | } 19 | 20 | var timeFormattedString: String { 21 | let seconds = self % 60 22 | let minutes = (self / 60) % 60 23 | let hours = (self / 3600) 24 | 25 | if hours == 0 { 26 | if minutes == 0 { 27 | return String(format: "%0.2d", seconds) 28 | } 29 | return String(format: "%0.2d:%0.2d", minutes, seconds) 30 | } 31 | return String(format: "%0.2d:%0.2d:%0.2d", hours, minutes, seconds) 32 | } 33 | } 34 | 35 | extension UInt64 { 36 | /// Return a string representing a formatted file size using the integers as bytes count. 37 | var fileSizeToFormattedString: String { 38 | let byteCountFormatter = ByteCountFormatter() 39 | byteCountFormatter.countStyle = .file 40 | return byteCountFormatter.string(for: self) ?? "Unknown Size" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /migrator/Extensions/LinearGradient-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinearGradient-Extension.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 14/12/2023. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | extension LinearGradient { 13 | /// Return BigButtonSelected custom linear gradient for the given color scheme. 14 | /// - Parameter colorScheme: the color scheme to use to calculate the LinearGradient. 15 | /// - Returns: custom LinearGradient. 16 | static func bigButtonSelected(colorScheme: ColorScheme) -> LinearGradient { 17 | switch colorScheme { 18 | case .dark: 19 | return LinearGradient(colors: [Color(red: 0.08627450980392157, green: 0.40784313725490196, blue: 0.8980392156862745), Color(red: 0.08627450980392157, green: 0.36470588235294116, blue: 0.807843137254902)], startPoint: .top, endPoint: .bottom) 20 | default: 21 | return LinearGradient(colors: [Color(red: 0, green: 0.47843137254901963, blue: 1)], startPoint: .top, endPoint: .bottom) 22 | } 23 | } 24 | 25 | /// Return ButtonSelected custom linear gradient for the given color scheme. 26 | /// - Parameter colorScheme: the color scheme to use to calculate the LinearGradient. 27 | /// - Returns: custom LinearGradient. 28 | static func buttonSelected(colorScheme: ColorScheme) -> LinearGradient { 29 | switch colorScheme { 30 | case .dark: 31 | return LinearGradient(colors: [Color(red: 0.08627450980392157, green: 0.40784313725490196, blue: 0.8980392156862745), Color(red: 0.08627450980392157, green: 0.36470588235294116, blue: 0.807843137254902)], startPoint: .top, endPoint: .bottom) 32 | default: 33 | return LinearGradient(colors: [Color(red: 0, green: 0.47843137254901963, blue: 1)], startPoint: .top, endPoint: .bottom) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /migrator/Extensions/NSTableView-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSTableView-Extension.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 05/01/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import AppKit 11 | 12 | /// Custom subclass of NSTableView 13 | extension NSTableView { 14 | /// Override the viewDidMoveToWindow method to customize the table view when it's added to a window 15 | open override func viewDidMoveToWindow() { 16 | super.viewDidMoveToWindow() 17 | 18 | backgroundColor = NSColor.clear 19 | enclosingScrollView?.drawsBackground = false 20 | enclosingScrollView?.verticalScroller = NoBackgroundScroller() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /migrator/Extensions/NWBrowser-Result-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NWBrowser-Result-Extension.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 15/11/2023. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Network 11 | 12 | extension NWBrowser.Result: @retroactive Identifiable { 13 | /// Provides a unique identifier for each NWBrowser.Result instance. 14 | public var id: Int { 15 | return self.hashValue 16 | } 17 | 18 | /// A computed property to derive a user-friendly name for the NWBrowser.Result. 19 | public var resultName: String { 20 | switch self.endpoint { 21 | case .hostPort: 22 | return "Unknown" 23 | case .service(name: let name, _, _, _): 24 | return name 25 | case .unix: 26 | return "Unknown" 27 | case .url: 28 | return "Unknown" 29 | case .opaque: 30 | return "Unknown" 31 | @unknown default: 32 | return "Unknown" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /migrator/Extensions/NWParameters-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NWParameters-Extension.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 11/01/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Network 11 | import CryptoKit 12 | 13 | extension NWParameters { 14 | /// Initializes `NWParameters` with TCP and custom TLS settings, including a pre-shared key for TLS authentication. 15 | /// - Parameter passcode: A `String` used to derive the pre-shared key for TLS authentication. 16 | convenience init(passcode: String) { 17 | let tcpOptions = NWProtocolTCP.Options() 18 | tcpOptions.enableKeepalive = true 19 | tcpOptions.keepaliveIdle = 1 20 | tcpOptions.keepaliveCount = 2 21 | tcpOptions.keepaliveInterval = 1 22 | 23 | // Initialize `NWParameters` with custom TLS options (derived from the passcode) and the specified TCP options. 24 | self.init(tls: NWParameters.tlsOptions(passcode: passcode), tcp: tcpOptions) 25 | self.includePeerToPeer = true 26 | self.attribution = .developer 27 | self.preferNoProxies = true 28 | 29 | // Configure custom application protocol using `MigratorNetworkProtocol`. 30 | let migratorOptions = NWProtocolFramer.Options(definition: MigratorNetworkProtocol.definition) 31 | self.defaultProtocolStack.applicationProtocols.insert(migratorOptions, at: 0) 32 | } 33 | 34 | /// Generates TLS options including a pre-shared key derived from a passcode. 35 | /// - Parameter passcode: The passcode used to derive the pre-shared key. 36 | /// - Returns: A configured `NWProtocolTLS.Options` instance. 37 | private static func tlsOptions(passcode: String) -> NWProtocolTLS.Options { 38 | let tlsOptions = NWProtocolTLS.Options() 39 | 40 | // Derive a symmetric key from the passcode. 41 | let authenticationKey = SymmetricKey(data: passcode.data(using: .utf8)!) 42 | // Create an authentication code for "MigratorNetworkProtocol" using HMAC with SHA256. 43 | let authenticationCode = HMAC.authenticationCode(for: "MigratorNetworkProtocol".data(using: .utf8)!, using: authenticationKey) 44 | 45 | // Convert the authentication code to `DispatchData`. 46 | let authenticationDispatchData = authenticationCode.withUnsafeBytes { 47 | DispatchData(bytes: $0) 48 | } 49 | // Add the pre-shared key to the TLS options. 50 | sec_protocol_options_add_pre_shared_key(tlsOptions.securityProtocolOptions, 51 | authenticationDispatchData as __DispatchData, 52 | stringToDispatchData("MigratorNetworkProtocol")! as __DispatchData) 53 | // Append the cipher suite `TLS_PSK_WITH_AES_128_GCM_SHA256` to the TLS options. 54 | sec_protocol_options_append_tls_ciphersuite(tlsOptions.securityProtocolOptions, 55 | tls_ciphersuite_t(rawValue: UInt16(TLS_PSK_WITH_AES_128_GCM_SHA256))!) 56 | return tlsOptions 57 | } 58 | 59 | /// Converts a `String` to `DispatchData`. 60 | /// - Parameter string: The `String` to be converted. 61 | /// - Returns: The converted `DispatchData`, or `nil` if the conversion fails. 62 | private static func stringToDispatchData(_ string: String) -> DispatchData? { 63 | guard let stringData = string.data(using: .utf8) else { 64 | return nil 65 | } 66 | let dispatchData = stringData.withUnsafeBytes { 67 | DispatchData(bytes: $0) 68 | } 69 | return dispatchData 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /migrator/Extensions/NWProtocolFramer-Message-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NWProtocolFramer-Message-Extension.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 30/01/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | import Network 12 | 13 | extension NWProtocolFramer.Message { 14 | /// A computed property to get the type of migration message from the message metadata. 15 | var migratorMessageType: MigratorMessageType { 16 | if let type = self["MigratorMessageType"] as? MigratorMessageType { 17 | return type 18 | } else { 19 | return .invalid 20 | } 21 | } 22 | 23 | /// A computed property to get the length of additional information included in the migration message. 24 | var migratorMessageInfoLenght: UInt32 { 25 | if let infoLenght = self["MigratorMessageInfoLenght"] as? UInt32 { 26 | return infoLenght 27 | } else { 28 | return 0 29 | } 30 | } 31 | 32 | /// Initializes a new `NWProtocolFramer.Message` with custom migration message type and information length. 33 | /// - Parameters: 34 | /// - migratorMessageType: The type of migration message. 35 | /// - infoLenght: The length of additional information included in the message. 36 | convenience init(migratorMessageType: MigratorMessageType, infoLenght: UInt32) { 37 | self.init(definition: MigratorNetworkProtocol.definition) 38 | self["MigratorMessageType"] = migratorMessageType 39 | self["MigratorMessageInfoLenght"] = infoLenght 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /migrator/Extensions/Notification-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notification-Extension.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 23/08/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | extension NSNotification.Name { 13 | static let devicePowerStatusChanged = NSNotification.Name("devicePowerStatusDidChange") 14 | } 15 | -------------------------------------------------------------------------------- /migrator/Extensions/Scene-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Scene-Extension.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 14/11/2023. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | extension Scene { 13 | /// Adjusts the window resizability based on the content size for macOS 13.0 and above. 14 | /// - Returns: A modified `Scene` with updated window resizability settings or the original `Scene` for older macOS versions. 15 | func windowResizabilityContentSize() -> some Scene { 16 | if #available(macOS 13.0, *) { 17 | return windowResizability(.contentSize) 18 | } else { 19 | return self 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /migrator/Extensions/String-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String-Extension.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 16/02/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | extension String { 13 | /// Return the localized instance of the string. 14 | public var localized: String { 15 | guard NSLocalizedString(self, comment: "") != self else { 16 | return self.replacingOccurrences(of: "\\n", with: "\n") 17 | } 18 | return NSLocalizedString(self, comment: "") 19 | } 20 | 21 | public var isNumber: Bool { 22 | return Int(self) != nil 23 | } 24 | 25 | func slice(from upperBound: String, to lowerBound: String) -> String? { 26 | return (range(of: upperBound)?.upperBound).flatMap { substringFrom in 27 | (range(of: lowerBound, range: substringFrom.. Bool { 19 | guard try resourceValues(forKeys: [.isDirectoryKey]).isDirectory == true else { 20 | return false 21 | } 22 | return try checkResourceIsReachable() 23 | } 24 | 25 | /// Calculates the total allocated size of files within the directory. 26 | /// - Parameter includingSubfolders: A Boolean value indicating whether to include subfolders in the calculation. 27 | /// - Returns: The total allocated size of files within the directory, or nil if the directory is not reachable or an error occurs. 28 | func directoryTotalAllocatedSize() async throws -> Int? { 29 | guard try isDirectoryAndReachable() else { return nil } 30 | guard let urls = FileManager.default.enumerator(at: self, includingPropertiesForKeys: nil)?.allObjects as? [URL] else { return nil } 31 | return try urls.lazy.reduce(0) { 32 | (try $1.resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize ?? 0) + $0 33 | } 34 | } 35 | 36 | func directoryTotalNumberOfFiles() async throws -> Int? { 37 | guard try isDirectoryAndReachable() else { return nil } 38 | guard let urls = FileManager.default.enumerator(at: self, includingPropertiesForKeys: nil)?.allObjects as? [URL] else { return nil } 39 | return urls.lazy.count 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /migrator/Extensions/View-Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View-Extension.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 05/01/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | extension View { 13 | /// Conditionally hides the view based on a Boolean flag. 14 | /// - Parameter isHidden: A Boolean value that determines whether the view should be hidden. 15 | /// - Returns: A view that is either hidden or visible based on the `isHidden` parameter. 16 | func hiddenConditionally(isHidden: Bool) -> some View { 17 | if isHidden { 18 | return AnyView(self.hidden()) 19 | } else { 20 | return AnyView(self) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /migrator/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSBonjourServices 6 | 7 | _migrator._tcp 8 | 9 | UIApplicationSceneManifest 10 | 11 | UIApplicationSupportsMultipleScenes 12 | 13 | UISceneConfigurations 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /migrator/MigratorApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MigratorApp.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 14/11/2023. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import AppKit 11 | import SwiftUI 12 | import IOKit.pwr_mgt 13 | 14 | @main 15 | struct MigratorApp: App { 16 | 17 | // Integrates a traditional AppDelegate to use functionalities not supported by SwiftUI. 18 | @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate 19 | 20 | // MARK: - State Variables 21 | 22 | @State private var currentWindow: NSWindow? 23 | @State private var showQuitConfirmationAlert: Bool = false 24 | 25 | // MARK: - Views 26 | 27 | // Defines the main content and behavior of the app's window. 28 | var body: some Scene { 29 | WindowGroup { 30 | MainView() 31 | .background(WindowAccessor(window: self.$currentWindow)) 32 | .onReceive(self.appDelegate.$userRequestToQuit, perform: { newValue in 33 | self.showQuitConfirmationAlert = newValue 34 | }) 35 | .alert(isPresented: self.$showQuitConfirmationAlert, content: { 36 | Alert(title: Text("common.app.attention"), 37 | message: Text(String(format: "common.app.quit.alert.message".localized, Bundle.main.name)), 38 | primaryButton: .cancel(), 39 | secondaryButton: .destructive(Text("common.app.quit.alert.button.quit"), action: { appDelegate.quit() })) 40 | }) 41 | } 42 | .commands { 43 | CommandGroup(replacing: .newItem) { } 44 | CommandGroup(replacing: .appVisibility) { } 45 | } 46 | .windowStyle(.hiddenTitleBar) 47 | .windowResizabilityContentSize() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /migrator/Model/DeviceManagement/ConfigProfile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigProfile.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 26/04/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// this struct describes a configuration profile. 13 | struct ConfigProfile: Decodable { 14 | 15 | // MARK: - Constants 16 | 17 | let name: String 18 | let organization: String? 19 | let installDate: Date? 20 | let identifier: String? 21 | let uuid: String? 22 | let removalDisallowed: Bool? 23 | let payloads: [ConfigProfilePayload]? 24 | 25 | // MARK: - Private Enums 26 | 27 | private enum CodingKeys: String, CodingKey { 28 | case name = "_name" 29 | case organization = "spconfigprofile_organization" 30 | case installDate = "spconfigprofile_install_date" 31 | case identifier = "spconfigprofile_profile_identifier" 32 | case uuid = "spconfigprofile_profile_uuid" 33 | case removalDisallowed = "spconfigprofile_RemovalDisallowed" 34 | case payloads = "_items" 35 | } 36 | 37 | // MARK: - Initializers 38 | 39 | init(from decoder: Decoder) throws { 40 | let container = try decoder.container(keyedBy: CodingKeys.self) 41 | self.name = try container.decode(String.self, forKey: .name) 42 | self.organization = try container.decodeIfPresent(String.self, forKey: .organization) 43 | let dateFormatter = DateFormatter() 44 | dateFormatter.locale = .current 45 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z" 46 | self.installDate = dateFormatter.date(from: try container.decodeIfPresent(String.self, forKey: .installDate)?.slice(from: "(", to: ")") ?? "") 47 | self.identifier = try container.decodeIfPresent(String.self, forKey: .identifier) 48 | self.uuid = try container.decodeIfPresent(String.self, forKey: .uuid) 49 | self.removalDisallowed = try container.decodeIfPresent(String.self, forKey: .removalDisallowed) ?? "no" == "yes" 50 | self.payloads = try container.decodeIfPresent([ConfigProfilePayload].self, forKey: .payloads) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /migrator/Model/DeviceManagement/ConfigProfilePayload.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigProfilePayload.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 26/04/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// This struct describes the payload of a configuration profile. 13 | struct ConfigProfilePayload: Decodable { 14 | 15 | // MARK: - Constants 16 | 17 | let name: String 18 | let organization: String? 19 | let identifier: String 20 | let uuid: String 21 | let serverURL: String? 22 | 23 | // MARK: - Private Enums 24 | 25 | private enum CodingKeys: String, CodingKey { 26 | case name = "_name" 27 | case organization = "spconfigprofile_organization" 28 | case identifier = "spconfigprofile_payload_identifier" 29 | case uuid = "spconfigprofile_payload_uuid" 30 | case serverURL = "spconfigprofile_payload_data" 31 | } 32 | 33 | // MARK: - Initializers 34 | 35 | init(from decoder: any Decoder) throws { 36 | let container = try decoder.container(keyedBy: CodingKeys.self) 37 | self.name = try container.decode(String.self, forKey: .name) 38 | self.organization = try container.decodeIfPresent(String.self, forKey: .organization) 39 | self.identifier = try container.decode(String.self, forKey: .identifier) 40 | self.uuid = try container.decode(String.self, forKey: .uuid) 41 | self.serverURL = (try container.decodeIfPresent(String.self, forKey: .serverURL))?.slice(from: "ServerURL = \"", to: "\"") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /migrator/Model/DeviceManagement/ConfigProfileSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigProfileSection.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 26/04/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// This struct represent a section of the configuration profiles installed on the device. 13 | struct ConfigProfileSection: Decodable { 14 | 15 | // MARK: - Constants 16 | 17 | let name: String 18 | let profiles: [ConfigProfile] 19 | 20 | // MARK: - Private Enums 21 | 22 | private enum CodingKeys: String, CodingKey { 23 | case name = "_name" 24 | case profiles = "_items" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /migrator/Model/DeviceManagement/ConfigurationProfileDataType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurationProfileDataType.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 26/04/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// This struct describe the system profiles Configuration Profile Data Type. 13 | struct ConfigurationProfileDataType: Decodable { 14 | 15 | // MARK: - Constants 16 | 17 | let sections: [ConfigProfileSection] 18 | 19 | // MARK: - Private Enums 20 | 21 | private enum CodingKeys: String, CodingKey { 22 | case sections = "SPConfigurationProfileDataType" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /migrator/Model/DeviceManagement/DeviceManagementState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceManagementState.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 26/04/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// Enum that represents the possible device management states. 13 | enum DeviceManagementState { 14 | case unmanaged 15 | case managed(env: ManagedEnvironment) 16 | case managedByUnknownOrg 17 | case unknown 18 | } 19 | -------------------------------------------------------------------------------- /migrator/Model/DeviceManagement/ManagedEnvironment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ManagedEnvironment.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 04/09/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// This struct represents a MDM environment. 13 | struct ManagedEnvironment { 14 | 15 | // MARK: - Variables 16 | 17 | var name: String 18 | var serverURL: String 19 | var reconPolicyID: String 20 | 21 | // MARK: - Initializers 22 | 23 | init(name: String, serverURL: String, reconPolicyID: String) { 24 | self.name = name 25 | self.reconPolicyID = reconPolicyID 26 | 27 | /// Allow for serverURL to be input without the "/mdm/ServerURL" suffix. 28 | if !serverURL.hasSuffix("/mdm/ServerURL") { 29 | if serverURL.hasSuffix("/") { 30 | self.serverURL = serverURL + "mdm/ServerURL" 31 | } else { 32 | self.serverURL = serverURL + "/mdm/ServerURL" 33 | } 34 | } else { 35 | /// The serverURL is already formatted correctly. 36 | self.serverURL = serverURL 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /migrator/Model/DuplicateFilesHandlingPolicy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DuplicateFilesHandlingPolicy.swift 3 | // migrator 4 | // 5 | // Created by Simone Martorelli on 29/09/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // 8 | 9 | /// The possible method to handle duplicate files on the destination device. 10 | enum DuplicateFilesHandlingPolicy: String, Codable { 11 | case ignore /// Ignore the new file received and keep the one present on the destination device. 12 | case overwrite /// Overwrite the file available on the destination device with the new one. 13 | case move /// Move the old file available on the destination device to a backup folder on the desktop. 14 | } 15 | -------------------------------------------------------------------------------- /migrator/Model/JamfReconMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JamfReconStyle.swift 3 | // migrator 4 | // 5 | // Created by Simone Martorelli on 29/09/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // 8 | 9 | /// The supported methods to run the jamf Inventory Update. 10 | enum JamfReconMethod: String, Codable { 11 | case direct /// Implies running `sudo jamf recon` command. The app will ask for the user Mac password. 12 | case selfServicePolicy /// Run a self service policy using deeplinks. The policy takes care of running the recon. The app track the recon in background. 13 | } 14 | -------------------------------------------------------------------------------- /migrator/Model/MigratorError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MigratorError.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 12/01/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// A tracked error. 13 | enum MigratorError { 14 | // Enum cases for different types of errors that can occur during migration 15 | case fileError(type: Enums.FileError) 16 | case connectionError(type: Enums.ConnectionError) 17 | case internalError(type: Enums.InternalError) 18 | 19 | // Nested enum namespace for file and connection errors 20 | class Enums { } 21 | } 22 | 23 | /// Conforming to the `LocalizedError` protocol to provide localized descriptions for errors 24 | extension MigratorError: LocalizedError { 25 | var errorDescription: String? { 26 | switch self { 27 | case .fileError(let type): 28 | return type.localizedDescription 29 | case .connectionError(let type): 30 | return type.localizedDescription 31 | case .internalError(let type): 32 | return type.localizedDescription 33 | } 34 | } 35 | } 36 | 37 | // MARK: - Model Errors 38 | 39 | /// Extension to `MigratorError.Enums` for file errors. 40 | extension MigratorError.Enums { 41 | /// Enum cases for different types of file errors. 42 | enum FileError { 43 | case noData 44 | case noInfo 45 | case failedDuringFileHandling(error: Error? = nil) 46 | case failedToWriteFile 47 | } 48 | /// Enum cases for different types of connection errors. 49 | enum ConnectionError { 50 | case failedToSendFile 51 | } 52 | /// Enum case for different types of app's internal errors. 53 | enum InternalError { 54 | case undefined 55 | case casting 56 | case data 57 | } 58 | } 59 | 60 | /// Conforming to the `LocalizedError` protocol to provide localized descriptions for file errors 61 | extension MigratorError.Enums.FileError: LocalizedError { 62 | var errorDescription: String? { 63 | switch self { 64 | case .noData: 65 | return "MigratorError FileError noData" 66 | case .noInfo: 67 | return "MigratorError FileError noInfo" 68 | case .failedDuringFileHandling: 69 | return "MigratorError FileError failedDuringFileHandling" 70 | case .failedToWriteFile: 71 | return "MigratorError FileError failedToWriteFile" 72 | } 73 | } 74 | } 75 | 76 | /// Conforming to the `LocalizedError` protocol to provide localized descriptions for connection errors 77 | extension MigratorError.Enums.ConnectionError: LocalizedError { 78 | var errorDescription: String? { 79 | switch self { 80 | case .failedToSendFile: 81 | return "MigratorError ConnectionError failed to send file" 82 | } 83 | } 84 | } 85 | 86 | /// Conforming to the `LocalizedError` protocol to provide localized descriptions for internal errors 87 | extension MigratorError.Enums.InternalError: LocalizedError { 88 | var errorDescription: String? { 89 | switch self { 90 | case .undefined: 91 | return "MigratorError InternalError undefined error" 92 | case .casting: 93 | return "MigratorError InternalError casting error" 94 | case .data: 95 | return "MigratorError InternalError data error" 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /migrator/Model/MigratorFileURL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MigratorFileURL.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 30/01/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// Struct representing a file URL in a migration context. 13 | struct MigratorFileURL: Codable, Equatable, Hashable { 14 | /// Enum defining different source directory types. 15 | enum MigratorURLSource: UInt8, Codable { 16 | case documentsFolder 17 | case desktopFolder 18 | case userFolder 19 | case applicationsFolder 20 | case unknown 21 | } 22 | 23 | // MARK: - Variables 24 | 25 | /// The source directory type. 26 | var source: MigratorURLSource 27 | 28 | // MARK: - Private Variables 29 | 30 | /// The relative path to the file. 31 | private var relativePath: String 32 | 33 | // MARK: - Initializers 34 | 35 | /// Initializes a `MigratorFileURL` instance with a given URL. 36 | /// - Parameter url: The URL to initialize from. 37 | init(with url: URL) { 38 | var relativePath = url.relativePath 39 | let userFolder = FileManager.default.homeDirectoryForCurrentUser.relativePath 40 | let documentsFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.relativePath 41 | let desktopFolder = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!.relativePath 42 | let applicationsFolder = FileManager.default.urls(for: .applicationDirectory, in: .localDomainMask).first!.relativePath 43 | 44 | if relativePath.contains("\(desktopFolder)") { 45 | relativePath.removeSubrange(Range(uncheckedBounds: (lower: desktopFolder.startIndex, upper: desktopFolder.endIndex))) 46 | if !relativePath.isEmpty { 47 | _ = relativePath.removeFirst() 48 | } 49 | self.source = .desktopFolder 50 | self.relativePath = relativePath 51 | } else if relativePath.contains("\(documentsFolder)") { 52 | relativePath.removeSubrange(Range(uncheckedBounds: (lower: documentsFolder.startIndex, upper: documentsFolder.endIndex))) 53 | if !relativePath.isEmpty { 54 | _ = relativePath.removeFirst() 55 | } 56 | self.source = .documentsFolder 57 | self.relativePath = relativePath 58 | } else if relativePath.contains("\(userFolder)") { 59 | relativePath.removeSubrange(Range(uncheckedBounds: (lower: userFolder.startIndex, upper: userFolder.endIndex))) 60 | if !relativePath.isEmpty { 61 | _ = relativePath.removeFirst() 62 | } 63 | self.source = .userFolder 64 | self.relativePath = relativePath 65 | } else if relativePath.contains("\(applicationsFolder)") { 66 | relativePath.removeSubrange(Range(uncheckedBounds: (lower: applicationsFolder.startIndex, upper: applicationsFolder.endIndex))) 67 | if !relativePath.isEmpty { 68 | _ = relativePath.removeFirst() 69 | } 70 | self.source = .applicationsFolder 71 | self.relativePath = relativePath 72 | } else { 73 | self.source = .unknown 74 | self.relativePath = relativePath 75 | } 76 | } 77 | 78 | /// The full URL based on the source and relative path. 79 | func fullURL() -> URL { 80 | switch source { 81 | case .documentsFolder: 82 | return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(relativePath) 83 | case .desktopFolder: 84 | return FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!.appendingPathComponent(relativePath) 85 | case .userFolder: 86 | return FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(relativePath) 87 | case .applicationsFolder: 88 | return FileManager.default.urls(for: .applicationDirectory, in: .localDomainMask).first!.appendingPathComponent(relativePath) 89 | case .unknown: 90 | return URL(string: relativePath)! 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /migrator/Model/MigratorPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MigratorPage.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 16/11/2023. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | /// Enum representing different pages in the migration process. 13 | enum MigratorPage: CaseIterable { 14 | case welcome 15 | case browser 16 | case codeVerification 17 | case server 18 | case migrationSetup 19 | case migration 20 | case appleID 21 | case recon 22 | case reboot 23 | case final 24 | 25 | /// Generates a SwiftUI view for the corresponding page. 26 | /// - Parameter action: A closure to handle actions triggered within the views. 27 | /// - Returns: A SwiftUI view representing the page. 28 | @ViewBuilder 29 | func view(action: @escaping (MigratorPage) -> Void) -> some View { 30 | switch self { 31 | case .welcome: 32 | WelcomeView(action: action) 33 | case .browser: 34 | BrowserView(action: action) 35 | case .codeVerification: 36 | CodeVerificationView(action: action) 37 | case .server: 38 | ServerView(action: action) 39 | case .migrationSetup: 40 | MigrationSetupView(action: action) 41 | case .migration: 42 | MigrationView(action: action) 43 | case .appleID: 44 | AppleIDView(action: action) 45 | case .recon: 46 | JamfReconView(action: action) 47 | case .reboot: 48 | RebootView(action: action) 49 | case .final: 50 | FinalView() 51 | } 52 | } 53 | 54 | func next() -> MigratorPage { 55 | switch self { 56 | case .server: 57 | if !AppContext.shouldSkipAppleIDCheck && !Utils.iCloudAvailable { 58 | return .appleID 59 | } else { 60 | fallthrough 61 | } 62 | case .appleID: 63 | if !AppContext.shouldSkipDeviceReboot { 64 | return .reboot 65 | } else { 66 | fallthrough 67 | } 68 | case .reboot: 69 | if AppContext.shouldSkipJamfRecon { 70 | return Utils.reconPage 71 | } else { 72 | fallthrough 73 | } 74 | case .recon: 75 | return .final 76 | default: 77 | return .welcome 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /migrator/Model/Network/CommunicationInterfaces.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommunicationInterfaces.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 30/01/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// Enumerates the types of communication interfaces a device may support. 13 | enum CommunicationInterfaces: CaseIterable { 14 | case wifi 15 | case thunderbolt 16 | case cellular 17 | } 18 | -------------------------------------------------------------------------------- /migrator/Model/Network/DefaultsMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultsMessage.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 05/09/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// Represents a message that describes a UserDefaults value. 13 | struct DefaultsMessage: Codable { 14 | 15 | // MARK: - Variables 16 | 17 | /// UserDefaults key. 18 | var key: String 19 | /// The boolean value if the app needs to trasfer a boolean default setting. 20 | var boolValue: Bool? 21 | /// The string value if the app needs to trasfer a string default setting. 22 | var stringValue: String? 23 | } 24 | -------------------------------------------------------------------------------- /migrator/Model/Network/FileMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileMessage.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 17/01/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// Represents a file and its associated metadata for transfer in the migration process. 13 | struct FileMessage { 14 | 15 | // MARK: - Variables 16 | 17 | /// Identifies a specific part of a file, used in multipart file transfers. 18 | var partNumber: Int 19 | /// File attributes such as modification date, size, etc., stored in a dictionary. 20 | var attributes: [FileAttributeKey: Any] = [:] 21 | /// The source file's URL, encapsulated in `MigratorFileURL` to include additional metadata. 22 | var source: MigratorFileURL 23 | 24 | // MARK: - Initializers 25 | 26 | /// Initializes a new `FileMessage` with the specified file URL, part number, and file attributes. 27 | /// - Parameters: 28 | /// - sourceFile: The URL of the source file. 29 | /// - part: The part number, defaulting to 0 for single-part files. 30 | /// - attributes: A dictionary of file attributes. 31 | init(with sourceFile: URL, part: Int = 0, attributes: [FileAttributeKey: Any] = [:]) { 32 | self.partNumber = part 33 | self.attributes = attributes 34 | self.source = MigratorFileURL(with: sourceFile) 35 | } 36 | } 37 | 38 | /// Extension to conform `FileMessage` to `Codable` for serialization and deserialization. 39 | extension FileMessage: Codable { 40 | /// Custom coding keys for encoding and decoding the properties of `FileMessage`. 41 | private enum CodingKeys: String, CodingKey { 42 | case partNumber 43 | case attributes 44 | case url 45 | } 46 | 47 | /// Decodes an instance of `FileMessage` from a decoder. 48 | init(from decoder: Decoder) throws { 49 | let container = try decoder.container(keyedBy: CodingKeys.self) 50 | self.partNumber = try container.decode(Int.self, forKey: .partNumber) 51 | self.source = try container.decode(MigratorFileURL.self, forKey: .url) 52 | 53 | // Decoding attributes requires special handling due to their diverse types. 54 | let attributesData = try container.decode([String: Data].self, forKey: .attributes) 55 | var attributes: [FileAttributeKey: Any] = [:] 56 | for (key, data) in attributesData { 57 | // Attempts to unarchive each attribute value from its Data representation. 58 | if let unarchivedObject = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSNumber.self, NSDate.self, NSString.self], from: data) { 59 | attributes[FileAttributeKey(key)] = unarchivedObject 60 | } 61 | } 62 | self.attributes = attributes 63 | } 64 | 65 | /// Encodes an instance of `FileMessage` to an encoder. 66 | func encode(to encoder: Encoder) throws { 67 | var container = encoder.container(keyedBy: CodingKeys.self) 68 | try container.encode(partNumber, forKey: .partNumber) 69 | try container.encode(source, forKey: .url) 70 | 71 | // Attributes are encoded as a dictionary mapping strings to Data. 72 | var attributesData = [String: Data]() 73 | for (key, value) in attributes { 74 | // Attempts to archive each attribute value into Data. 75 | if let data = try? NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: true) { 76 | attributesData[key.rawValue] = data 77 | } 78 | } 79 | try container.encode(attributesData, forKey: .attributes) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /migrator/Model/Network/MigratorMessageType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MigratorMessageType.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 30/01/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// Defines the types of messages that can be sent and received in the migration process. 13 | enum MigratorMessageType: UInt32 { 14 | case hostname = 0 // Represents a message containing the hostname of a device. 15 | case file = 1 // Represents a message containing a file, typically used for transferring a single file. 16 | case multipartFile = 2 // Represents a part of a file, used in scenarios where large files are split into multiple parts for transfer. 17 | case availableSpace = 3 // Represents a message containing information about the available storage space on a device. 18 | case result = 4 // Represents a message that contains the result of a requested operation, such as the success or failure of a file transfer. 19 | case invalid = 5 // Represents an invalid or unrecognized message type, typically used for error handling. 20 | case symlink = 6 // Represents a message containing a symbolic link, including its source and target paths. 21 | case directory = 7 // Represents a message indicating a directory, possibly used when initiating the transfer of a directory's contents. 22 | case metadata = 8 // Represents a metadata message. 23 | case defaults = 9 // Represents a message with UserDefaults values. 24 | } 25 | -------------------------------------------------------------------------------- /migrator/Model/Network/MigratorNetworkProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MigratorNetworkProtocol.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 11/01/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | import Network 12 | 13 | /// Custom network protocol for the migrator, implementing framing for network messages. 14 | class MigratorNetworkProtocol: NWProtocolFramerImplementation { 15 | 16 | // MARK: - Static Variables 17 | 18 | /// Definition for the custom network protocol, used to create protocol instances. 19 | static let definition = NWProtocolFramer.Definition(implementation: MigratorNetworkProtocol.self) 20 | 21 | // MARK: - Static Constants 22 | 23 | /// Label for the protocol, useful for debugging and logging. 24 | static var label: String { return "MigratorNetworkProtocol" } 25 | 26 | // MARK: - Private Constants 27 | 28 | /// Logger instance. 29 | private let logger: MLogger = MLogger.main 30 | 31 | // MARK: - Initializers 32 | 33 | /// Required initializer for the protocol framer instance. 34 | required init(framer: NWProtocolFramer.Instance) { } 35 | 36 | // MARK: - Public Methods 37 | 38 | /// Called to start the protocol. Indicates the framer is ready to process data. 39 | func start(framer: NWProtocolFramer.Instance) -> NWProtocolFramer.StartResult { return .ready } 40 | 41 | /// Called when the framer needs to be woken up, but typically not used for stateless protocols. 42 | func wakeup(framer: NWProtocolFramer.Instance) { } 43 | 44 | /// Called to stop the protocol. Returns true to indicate the stop was handled. 45 | func stop(framer: NWProtocolFramer.Instance) -> Bool { return true } 46 | 47 | /// Cleans up any state or resources when the protocol is no longer needed. 48 | func cleanup(framer: NWProtocolFramer.Instance) { } 49 | 50 | /// Handles outgoing data, adding protocol-specific framing before sending. 51 | /// - Parameters: 52 | /// - framer: The protocol framer instance. 53 | /// - message: The message being sent. 54 | /// - messageLength: The length of the message payload. 55 | /// - isComplete: Whether this is the complete message. 56 | func handleOutput(framer: NWProtocolFramer.Instance, message: NWProtocolFramer.Message, messageLength: Int, isComplete: Bool) { 57 | // Prepares the custom protocol header with message type and length. 58 | let type = message.migratorMessageType 59 | let infoLength = message.migratorMessageInfoLenght 60 | let header = MigratorNetworkProtocolHeader(type: type.rawValue, length: UInt32(messageLength), infoLength: infoLength) 61 | 62 | // Writes the header to the output data stream. 63 | framer.writeOutput(data: header.encodedData) 64 | 65 | // Writes the message payload to the output data stream without copying, for efficiency. 66 | do { 67 | try framer.writeOutputNoCopy(length: messageLength) 68 | } catch let error { 69 | logger.log("migratorNetworkProtocol.handleOutput: failed to write output with error \(error.localizedDescription)", type: .error) 70 | } 71 | } 72 | 73 | /// Handles incoming data, removing protocol-specific framing and delivering payloads. 74 | /// - Parameter framer: The protocol framer instance. 75 | /// - Returns: The number of bytes consumed from the input stream. 76 | func handleInput(framer: NWProtocolFramer.Instance) -> Int { 77 | while true { 78 | var tempHeader: MigratorNetworkProtocolHeader? 79 | let headerSize = MigratorNetworkProtocolHeader.encodedSize 80 | 81 | // Attempts to parse the header from the incoming data stream. 82 | let parsed = framer.parseInput(minimumIncompleteLength: headerSize, 83 | maximumLength: headerSize) { (buffer, _) -> Int in 84 | guard let buffer = buffer else { 85 | return 0 86 | } 87 | if buffer.count < headerSize { 88 | return 0 // Incomplete header, needs more data. 89 | } 90 | tempHeader = MigratorNetworkProtocolHeader(buffer) 91 | return headerSize 92 | } 93 | 94 | // Proceeds only if a complete header was successfully parsed. 95 | guard parsed, let header = tempHeader else { 96 | return headerSize // Returns the expected size of the header to wait for more data. 97 | } 98 | 99 | // Determines the message type from the header. 100 | var messageType = MigratorMessageType.invalid 101 | if let parsedMessageType = MigratorMessageType(rawValue: header.type) { 102 | messageType = parsedMessageType 103 | } 104 | 105 | // Creates a message with the parsed type and delivers it along with the payload. 106 | let message = NWProtocolFramer.Message(migratorMessageType: messageType, infoLenght: header.infoLength) 107 | if !framer.deliverInputNoCopy(length: Int(header.length), message: message, isComplete: true) { 108 | return 0 // If delivery fails, indicates no bytes were consumed. 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /migrator/Model/Network/MigratorNetworkProtocolHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MigratorNetworkProtocolHeader.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 30/01/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// Represents the header for messages sent using the `MigratorNetworkProtocol`. 13 | struct MigratorNetworkProtocolHeader: Codable { 14 | 15 | // MARK: - Static Variables 16 | 17 | /// The size of the encoded header, calculated based on the size of its `UInt32` properties. 18 | static var encodedSize: Int { 19 | return (MemoryLayout.size * 3) 20 | } 21 | 22 | // MARK: - Constants 23 | 24 | /// The type of the message, used to identify the kind of operation or data being sent. 25 | let type: UInt32 26 | /// The length of the message payload, allowing the receiver to know how much data to expect. 27 | let length: UInt32 28 | /// An additional field specifying the length of informational data, if any, included in the message. 29 | let infoLength: UInt32 30 | 31 | // MARK: - Variables 32 | 33 | /// Encodes the header into a `Data` object, suitable for transmission. 34 | var encodedData: Data { 35 | var tempType = type 36 | var tempLength = length 37 | var tempInfoLength = infoLength 38 | 39 | var data = Data(bytes: &tempType, count: MemoryLayout.size) 40 | data.append(Data(bytes: &tempLength, count: MemoryLayout.size)) 41 | data.append(Data(bytes: &tempInfoLength, count: MemoryLayout.size)) 42 | return data 43 | } 44 | 45 | // MARK: - Initializers 46 | 47 | /// Initializes a new header with the specified message type, payload length, and optional info length. 48 | /// - Parameters: 49 | /// - type: The type identifier for the message. 50 | /// - length: The length of the message payload. 51 | /// - infoLength: The length of any additional informational data included in the message (default is 0). 52 | init(type: UInt32, length: UInt32, infoLength: UInt32 = 0) { 53 | self.type = type 54 | self.length = length 55 | self.infoLength = infoLength 56 | } 57 | 58 | /// Initializes a header from a raw buffer, typically used when parsing incoming data. 59 | /// - Parameter buffer: The buffer containing the raw bytes of the header. 60 | init(_ buffer: UnsafeMutableRawBufferPointer) { 61 | var tempType: UInt32 = 0 62 | var tempLength: UInt32 = 0 63 | var tempInfoLength: UInt32 = 0 64 | 65 | withUnsafeMutableBytes(of: &tempType) { typePtr in 66 | typePtr.copyMemory(from: UnsafeRawBufferPointer(start: buffer.baseAddress!.advanced(by: 0), 67 | count: MemoryLayout.size)) 68 | } 69 | withUnsafeMutableBytes(of: &tempLength) { lengthPtr in 70 | lengthPtr.copyMemory(from: UnsafeRawBufferPointer(start: buffer.baseAddress!.advanced(by: MemoryLayout.size), 71 | count: MemoryLayout.size)) 72 | } 73 | withUnsafeMutableBytes(of: &tempInfoLength) { infoLengthPtr in 74 | infoLengthPtr.copyMemory(from: UnsafeRawBufferPointer(start: buffer.baseAddress!.advanced(by: MemoryLayout.size * 2), 75 | count: MemoryLayout.size)) 76 | } 77 | 78 | type = tempType 79 | length = tempLength 80 | infoLength = tempInfoLength 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /migrator/Model/Network/NetworkDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkDevice.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 16/11/2023. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | import Network 12 | 13 | /// Represents a network device discovered by `NWBrowser`. 14 | struct NetworkDevice { 15 | 16 | // MARK: - Variables 17 | 18 | /// The raw browser result containing detailed information about the network service. 19 | var browserResult: NWBrowser.Result 20 | /// List of communication interfaces supported by the device, filtered and mapped from the browser result. 21 | var interfaces: [CommunicationInterfaces] 22 | /// Human-readable name for the device, determined based on the type of endpoint in the browser result. 23 | var name: String 24 | /// Unique identifier derived from the browser result, ensuring each device is uniquely identifiable. 25 | var id: Int { 26 | return browserResult.id 27 | } 28 | 29 | // MARK: - Initializers 30 | 31 | /// Initializes a new `NetworkDevice` from a `NWBrowser.Result`. 32 | /// - Parameter browserResult: The result from a network browser used to discover the device. 33 | init(browserResult: NWBrowser.Result) { 34 | self.browserResult = browserResult 35 | 36 | // Determines the device name based on the endpoint type, defaulting to "Unknown" for unrecognized types. 37 | self.name = { 38 | switch browserResult.endpoint { 39 | case .hostPort: 40 | return "Unknown" 41 | case .service(name: let name, _, _, _): 42 | return name 43 | case .unix: 44 | return "Unknown" 45 | case .url: 46 | return "Unknown" 47 | case .opaque: 48 | return "Unknown" 49 | @unknown default: 50 | return "Unknown" 51 | } 52 | }() 53 | 54 | // Maps the interfaces reported by the browser result to a predefined list of `CommunicationInterfaces`. 55 | let mappedInterfaces: [CommunicationInterfaces] = browserResult.interfaces.compactMap { interface in 56 | switch interface.type { 57 | case .other: 58 | return nil 59 | case .wifi: 60 | return CommunicationInterfaces.wifi 61 | case .cellular: 62 | return CommunicationInterfaces.cellular 63 | case .wiredEthernet: 64 | return CommunicationInterfaces.thunderbolt 65 | case .loopback: 66 | return nil 67 | @unknown default: 68 | return nil 69 | } 70 | } 71 | 72 | // Removes duplicates and sorts the interfaces for consistent ordering. 73 | self.interfaces = Set(mappedInterfaces).sorted(by: ==) 74 | } 75 | } 76 | 77 | extension NetworkDevice: Identifiable, Hashable { 78 | 79 | // MARK: - Conformance to `Equatable` 80 | 81 | static func == (lhs: NetworkDevice, rhs: NetworkDevice) -> Bool { 82 | return lhs.id == rhs.id 83 | } 84 | 85 | // MARK: - Conformance to `Hashable` 86 | 87 | /// Provides a hash value for the object, allowing it to be used in hash-based collections. 88 | func hash(into hasher: inout Hasher) { 89 | hasher.combine(id) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /migrator/Model/Network/SymbolicLinkMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SymbolicLinkMessage.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 30/01/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Foundation 11 | 12 | /// Represents a message that describes a symbolic link for the migration process. 13 | struct SymbolicLinkMessage: Codable { 14 | 15 | // MARK: - Variables 16 | 17 | /// The source location of the symbolic link, encapsulated in a `MigratorFileURL` to include any necessary metadata or formatting. 18 | var source: MigratorFileURL 19 | /// The absolute destination path of the symbolic link, also encapsulated in a `MigratorFileURL`. 20 | /// This is used when the relative path is not applicable or when an absolute path is necessary for clarity or functionality. 21 | var absoluteDestination: MigratorFileURL 22 | /// An optional relative destination path for the symbolic link. This can be used in lieu of the absolute path 23 | /// when the destination is relative to a specific directory or when preserving the relative structure is important. 24 | var relativeDestination: String? 25 | } 26 | -------------------------------------------------------------------------------- /migrator/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x@2x.png -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x@2x.png -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x@2x.png -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x@2x.png -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/migrator/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x.png -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/Colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/Colors/bigButtonUnselected.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "osx", 6 | "reference" : "alternatingContentBackgroundColor" 7 | }, 8 | "idiom" : "universal" 9 | }, 10 | { 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "color" : { 18 | "color-space" : "srgb", 19 | "components" : { 20 | "alpha" : "0.100", 21 | "blue" : "255", 22 | "green" : "255", 23 | "red" : "255" 24 | } 25 | }, 26 | "idiom" : "universal" 27 | } 28 | ], 29 | "info" : { 30 | "author" : "xcode", 31 | "version" : 1 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/Colors/buttonUnselected.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "osx", 6 | "reference" : "controlBackgroundColor" 7 | }, 8 | "idiom" : "universal" 9 | }, 10 | { 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "color" : { 18 | "color-space" : "srgb", 19 | "components" : { 20 | "alpha" : "0.100", 21 | "blue" : "255", 22 | "green" : "255", 23 | "red" : "255" 24 | } 25 | }, 26 | "idiom" : "universal" 27 | } 28 | ], 29 | "info" : { 30 | "author" : "xcode", 31 | "version" : 1 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/Colors/discoveryViewBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "246", 9 | "green" : "246", 10 | "red" : "246" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "54", 27 | "green" : "54", 28 | "red" : "54" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/apple_id_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "apple_id_logo.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/apple_id_icon.imageset/apple_id_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/migrator/Resources/Assets.xcassets/apple_id_icon.imageset/apple_id_logo.png -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/apps_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "appsIcon.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/apps_icon.imageset/appsIcon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/migrator/Resources/Assets.xcassets/apps_icon.imageset/appsIcon.pdf -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/folder_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "folderIcon.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/folder_icon.imageset/folderIcon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/migrator/Resources/Assets.xcassets/folder_icon.imageset/folderIcon.pdf -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/icloud_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icloud.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/icloud_icon.imageset/icloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/migrator/Resources/Assets.xcassets/icloud_icon.imageset/icloud.png -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/icon.imageset/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/migrator/Resources/Assets.xcassets/icon.imageset/icon.png -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/new_mac.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "new_mac.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/new_mac.imageset/new_mac.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/migrator/Resources/Assets.xcassets/new_mac.imageset/new_mac.pdf -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/old_mac.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "old_mac.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/old_mac.imageset/old_mac.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/migrator/Resources/Assets.xcassets/old_mac.imageset/old_mac.pdf -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/preferences_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "preferencesIcon.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /migrator/Resources/Assets.xcassets/preferences_icon.imageset/preferencesIcon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IBM/mac-ibm-migration-tool/ef98f55a14413e76cb399b3110b56d05590300cb/migrator/Resources/Assets.xcassets/preferences_icon.imageset/preferencesIcon.pdf -------------------------------------------------------------------------------- /migrator/Resources/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 14/11/2023. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import os.log 11 | import Foundation 12 | import AppKit 13 | 14 | /// Utility functions for logging messages, retrieving interface styles, free space on the device, 15 | /// and getting the user's folder name. 16 | struct Utils { 17 | /// Retrieves the free space on the device. 18 | static var freeSpaceOnDevice: Int { 19 | let fileURL = FileManager.default.homeDirectoryForCurrentUser 20 | do { 21 | let values = try fileURL.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey]) 22 | if let capacity = values.volumeAvailableCapacityForImportantUsage { 23 | let freeSpace = Int(capacity) 24 | return freeSpace 25 | } else { 26 | return 0 27 | } 28 | } catch { 29 | return 0 30 | } 31 | } 32 | 33 | /// Retrieves the user's folder name. 34 | static var userFolderName: String { 35 | return FileManager.default.homeDirectoryForCurrentUser.lastPathComponent 36 | } 37 | 38 | /// Retrieves the status of iCloud on the device. 39 | static var iCloudAvailable: Bool { 40 | return FileManager.default.ubiquityIdentityToken != nil 41 | } 42 | 43 | /// The correct page to use for runnin Jamf Inventory Update based on the desired method. 44 | static var reconPage: MigratorPage { 45 | switch AppContext.jamfReconMethod { 46 | case .direct: 47 | return .recon 48 | case .selfServicePolicy: 49 | return .final 50 | } 51 | } 52 | 53 | static var systemSettingsLabel: String { 54 | if #available(macOS 13.0, *) { 55 | return "common.system.settings.ventura.label".localized 56 | } else { 57 | return "common.system.settings.pre.ventura.label".localized 58 | } 59 | } 60 | 61 | /// Generate a random code. 62 | /// - Parameter digits: number of digits of the code. 63 | /// - Returns: string with the generated code. 64 | static func generateRandomCode(digits: Int) -> String { 65 | var number = String() 66 | for _ in 1...digits { 67 | number += "\(Int.random(in: 1...9))" 68 | } 69 | return number 70 | } 71 | 72 | /// Prevents the display from sleeping. 73 | static func preventSleep() { 74 | MLogger.main.log("utils.preventSleep: Preventing Mac to enter sleep.", type: .default) 75 | var assertionID: IOPMAssertionID = IOPMAssertionID(0) 76 | _ = IOPMAssertionCreateWithName(kIOPMAssertPreventUserIdleDisplaySleep as CFString, IOPMAssertionLevel(kIOPMAssertionLevelOn), "Prevent display sleep during critical operations" as CFString, &assertionID) == kIOReturnSuccess 77 | 78 | } 79 | 80 | /// Restart the device using an Apple Script to communicate with "System Events" 81 | static func rebootMac() { 82 | MLogger.main.log("utils.rebootMac: Restarting Mac.", type: .default) 83 | var errors: NSDictionary? 84 | _ = NSAppleScript(source: "tell app \"System Events\" to restart")?.executeAndReturnError(&errors) 85 | } 86 | 87 | /// Install a Launch Agent on the device to restart the app once the planned reboot happen. 88 | static func installLaunchAgent() async { 89 | MLogger.main.log("utils.installLaunchAgent: Creating Launch Agent plist.", type: .default) 90 | let fileContent = """ 91 | 92 | 93 | 94 | 95 | Label 96 | mac.ibm.shift 97 | ProgramArguments 98 | 99 | /Applications/\(Bundle.main.name).app/Contents/MacOS/\(Bundle.main.name) 100 | 101 | ProcessType 102 | Interactive 103 | RunAtLoad 104 | 105 | KeepAlive 106 | 107 | 108 | 109 | """ 110 | FileManager.default.createFile(atPath: FileManager.default.homeDirectoryForCurrentUser.relativePath + "/Library/LaunchAgents/com.ibm.cio.be.migrator.plist", contents: fileContent.data(using: .utf8)!) 111 | MLogger.main.log("utils.installLaunchAgent: Loading Launch Agent.", type: .default) 112 | await withCheckedContinuation { continuation in 113 | _ = NSAppleScript(source: "do shell script \"launchctl bootstrap gui/501 \(FileManager.default.homeDirectoryForCurrentUser.relativePath + "/Library/LaunchAgents/com.ibm.cio.be.migrator.plist") \"")?.executeAndReturnError(nil) 114 | continuation.resume() 115 | } 116 | MLogger.main.log("utils.installLaunchAgent: Launch Agent installed.", type: .default) 117 | } 118 | 119 | /// Remove the Launch Agent from the device. 120 | static func removeLaunchAgent() async { 121 | MLogger.main.log("utils.removeLaunchAgent: Unloading Launch Agent.", type: .default) 122 | await withCheckedContinuation { continuation in 123 | _ = NSAppleScript(source: "do shell script \"launchctl bootout gui/501 \(FileManager.default.homeDirectoryForCurrentUser.relativePath + "/Library/LaunchAgents/com.ibm.cio.be.migrator.plist") \"")?.executeAndReturnError(nil) 124 | continuation.resume() 125 | } 126 | MLogger.main.log("utils.removeLaunchAgent: Removing Launch Agent plist.", type: .default) 127 | do { 128 | try FileManager.default.removeItem(atPath: FileManager.default.homeDirectoryForCurrentUser.relativePath + "/Library/LaunchAgents/com.ibm.cio.be.migrator.plist") 129 | MLogger.main.log("utils.removeLaunchAgent: Launch Agent removed.", type: .default) 130 | } catch { 131 | MLogger.main.log("utils.removeLaunchAgent: Error encountered while removing Launch Agent plist. Error: \(error.localizedDescription).", type: .default) 132 | } 133 | } 134 | 135 | /// Set the window level for the app to "floating" or "normal" based on the flag. 136 | /// - Parameter floating: if true the window level is set to "floating", if false the window leve is set to "normal" 137 | static func makeWindowFloating(_ floating: Bool = true) { 138 | MLogger.main.log("utils.makeWindowFloating: Setting app windows level to \(floating ? "floating" : "normal").", type: .default) 139 | for window in NSApplication.shared.windows { 140 | window.level = floating ? .floating : .normal 141 | } 142 | } 143 | 144 | /// Remove the `Close` UI button from the app Toolbar. 145 | static func removeClosableToolbarElement() { 146 | for window in NSApplication.shared.windows { 147 | window.styleMask.subtract(.closable) 148 | } 149 | } 150 | 151 | /// Restore the `Close` UI button in the app Toolbar. 152 | static func restoreClosableToolbarElement() { 153 | for window in NSApplication.shared.windows { 154 | window.styleMask.update(with: .closable) 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /migrator/Views/AppleIDView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppleIDView.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 15/08/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | /// Struct representing the page to visualize the Apple ID status and remediation steps. 13 | struct AppleIDView: View { 14 | 15 | // MARK: - Private Constants 16 | 17 | /// Logger instance. 18 | private let logger: MLogger = MLogger.main 19 | 20 | // MARK: - Constants 21 | 22 | /// Closure to execute when an action requires navigation to a different page. 23 | let action: (MigratorPage) -> Void 24 | 25 | // MARK: - Views 26 | 27 | var body: some View { 28 | VStack { 29 | Image("apple_id_icon") 30 | .resizable() 31 | .frame(width: 86, height: 86) 32 | .padding(.top, 55) 33 | .padding(.bottom, 8) 34 | Text("icloud.page.title.label") 35 | .multilineTextAlignment(.center) 36 | .font(.system(size: 27, weight: .bold)) 37 | .padding(.bottom, 8) 38 | Text(String(format: "icloud.page.body.label".localized, Utils.systemSettingsLabel, "icloud.page.main.button.label".localized)) 39 | .multilineTextAlignment(.center) 40 | .padding(.bottom) 41 | .padding(.horizontal, 40) 42 | Spacer() 43 | Button(action: { 44 | logger.log("appleIDView.mainButtonAction: Opening Apple ID Preferences Panel", type: .default) 45 | NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preferences.AppleIDPrefPane")!) 46 | }, label: { 47 | Text(String(format: "icloud.page.system.settings.button.label".localized, Utils.systemSettingsLabel)) 48 | .padding(4) 49 | }) 50 | .keyboardShortcut(.defaultAction) 51 | .padding(.bottom, 6) 52 | Divider() 53 | HStack { 54 | ProgressView() 55 | .controlSize(.small) 56 | Spacer() 57 | Button(action: { 58 | goToNextPage() 59 | }, label: { 60 | mainButtonLabel 61 | }) 62 | .buttonStyle(.bordered) 63 | .keyboardShortcut(.cancelAction) 64 | .padding(.leading, 6) 65 | } 66 | .padding(EdgeInsets(top: 8, leading: 16, bottom: 16, trailing: 16)) 67 | } 68 | .onReceive(NotificationCenter.default.publisher(for: Notification.Name.NSUbiquityIdentityDidChange)) { _ in 69 | if FileManager.default.ubiquityIdentityToken != nil { goToNextPage() } 70 | } 71 | .onAppear { 72 | Utils.makeWindowFloating(false) 73 | } 74 | } 75 | 76 | var mainButtonLabel: some View { 77 | Text("icloud.page.main.button.label") 78 | .padding(4) 79 | } 80 | 81 | // MARK: - Private Methods 82 | 83 | private func goToNextPage() { 84 | action(.appleID.next()) 85 | } 86 | } 87 | 88 | #Preview { 89 | AppleIDView(action: { _ in }) 90 | .frame(width: 812, height: 600) 91 | } 92 | -------------------------------------------------------------------------------- /migrator/Views/BrowserView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowserView.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 14/12/2023. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | /// Struct representing the page to visualize the Browser behaviour. 13 | struct BrowserView: View { 14 | 15 | // MARK: - Environment Variables 16 | 17 | @Environment(\.colorScheme) var colorScheme 18 | 19 | // MARK: - Constants 20 | 21 | /// Closure to execute when an action requires navigation to a different page. 22 | let action: (MigratorPage) -> Void 23 | /// The previous page to navigate back to, by default set to the welcome page. 24 | let previousPage: MigratorPage = .welcome 25 | /// The next page to navigate forward to, typically the migration setup page. 26 | let nextPage: MigratorPage = .codeVerification 27 | 28 | // MARK: - Observable Variables 29 | 30 | /// Observable object to control and observe migration browser-related activities. 31 | @ObservedObject var migrationController: MigrationController = MigrationController.shared 32 | 33 | // MARK: - State Variables 34 | 35 | /// State variable that store the selected network device result from the browsing action. 36 | @State private var selectedResult: NetworkDevice? 37 | 38 | // MARK: - Initializers 39 | 40 | /// Custom initializer to set up the view with a navigation action and start the browsing process. 41 | /// - Parameter action: the navigation action. 42 | init(action: @escaping (MigratorPage) -> Void) { 43 | self.action = action 44 | // Starts the browsing process as soon as the view is initialized. 45 | migrationController.startBrowser() 46 | } 47 | 48 | // MARK: - Views 49 | 50 | var body: some View { 51 | VStack { 52 | Image("icon") 53 | .resizable() 54 | .frame(width: 86, height: 86) 55 | .padding(.top, 55) 56 | .padding(.bottom, 8) 57 | Text("browser.page.title") 58 | .multilineTextAlignment(.center) 59 | .font(.system(size: 27, weight: .bold)) 60 | .padding(.bottom, 8) 61 | Text("browser.page.subtitle") 62 | .multilineTextAlignment(.center) 63 | .padding(.bottom) 64 | .padding(.horizontal, 40) 65 | deviceList 66 | .padding(.horizontal, 270) 67 | .padding(.bottom, 4) 68 | Spacer() 69 | Text(String(format: "browser.page.reminder.label".localized, Bundle.main.name, "welcome.page.button.big.left.label".localized)) 70 | .padding(.bottom, 4) 71 | .multilineTextAlignment(.center) 72 | Divider() 73 | HStack { 74 | Spacer() 75 | Button(action: { 76 | didPressSecondaryButton() 77 | }, label: { 78 | secondaryButtonLabel 79 | }) 80 | .buttonStyle(.bordered) 81 | .keyboardShortcut(.cancelAction) 82 | ZStack { 83 | Button(action: { 84 | didPressMainButton() 85 | }, label: { 86 | mainButtonLabel 87 | }) 88 | .buttonStyle(.bordered) 89 | .disabled(selectedResult == nil) 90 | .keyboardShortcut(.defaultAction) 91 | } 92 | .padding(.leading, 6) 93 | } 94 | .padding(EdgeInsets(top: 8, leading: 16, bottom: 16, trailing: 16)) 95 | } 96 | } 97 | 98 | var deviceList: some View { 99 | ZStack { 100 | Color("discoveryViewBackground") 101 | .clipShape(RoundedRectangle(cornerRadius: 20)) 102 | .shadow(color: Color(red: 0, green: 0, blue: 0, opacity: 0.15), radius: 6, x: 0, y: 0) 103 | VStack(alignment: .center) { 104 | ProgressView() 105 | .progressViewStyle(.circular) 106 | .controlSize(.small) 107 | .padding(.top, 10) 108 | List($migrationController.browserResults, id: \.self, selection: $selectedResult) { result in 109 | DeviceListRow(result: result) 110 | } 111 | .onChange(of: migrationController.browserResults) { newResults in 112 | if !newResults.contains(where: { $0.id == selectedResult?.id }) { 113 | selectedResult = nil 114 | } 115 | } 116 | .padding(.top, 0) 117 | .padding(.bottom, 8) 118 | .padding(.horizontal, 2) 119 | Spacer() 120 | } 121 | } 122 | } 123 | 124 | var mainButtonLabel: some View { 125 | Text("browser.page.button.main.label") 126 | .padding(4) 127 | } 128 | 129 | var secondaryButtonLabel: some View { 130 | Text("browser.page.button.secondary.label") 131 | .padding(4) 132 | } 133 | 134 | // MARK: - Private Methods 135 | 136 | private func didPressMainButton() { 137 | if let result = selectedResult?.browserResult { 138 | migrationController.selectedBrowserResult = result 139 | action(nextPage) 140 | } 141 | } 142 | 143 | private func didPressSecondaryButton() { 144 | migrationController.stopBrowser() 145 | action(previousPage) 146 | } 147 | } 148 | 149 | #Preview { 150 | BrowserView(action: { _ in }) 151 | .frame(width: 812, height: 600) 152 | } 153 | -------------------------------------------------------------------------------- /migrator/Views/CodeVerificationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodeVerificationView.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 07/03/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | import Combine 12 | 13 | struct CodeVerificationView: View { 14 | // MARK: - Environment Variables 15 | 16 | @Environment(\.colorScheme) var colorScheme 17 | 18 | // MARK: - Constants 19 | 20 | /// Closure to execute when an action requires navigation to a different page. 21 | let action: (MigratorPage) -> Void 22 | /// The previous page to navigate back to, by default set to the welcome page. 23 | let previousPage: MigratorPage = .browser 24 | /// The next page to navigate forward to, typically the migration setup page. 25 | let nextPage: MigratorPage = .migrationSetup 26 | 27 | // MARK: - Observable Variables 28 | 29 | /// Observable object to control and observe migration browser-related activities. 30 | @ObservedObject var migrationController: MigrationController = MigrationController.shared 31 | 32 | // MARK: - Focus State Variables 33 | 34 | /// Focus state of the code verification field. 35 | @FocusState private var isTextFieldFocused: Bool 36 | 37 | // MARK: - State Variables 38 | 39 | @State private var verificationCode: String = "" 40 | /// State variable to indicate whether the browser is in a loading state. 41 | @State private var isLoading: Bool = false 42 | /// Tracks the visibility of the connection error alert. 43 | @State private var showConnectionError: Bool = false 44 | /// Tracks the visibility of the code error alert. 45 | @State private var showCodeError: Bool = false 46 | 47 | // MARK: - Views 48 | 49 | var body: some View { 50 | VStack { 51 | Image("icon") 52 | .resizable() 53 | .frame(width: 86, height: 86) 54 | .padding(.top, 55) 55 | .padding(.bottom, 8) 56 | Text("code.verification.page.title") 57 | .multilineTextAlignment(.center) 58 | .font(.system(size: 27, weight: .bold)) 59 | .padding(.bottom, 8) 60 | Text("code.verification.page.subtitle") 61 | .multilineTextAlignment(.center) 62 | .padding(.bottom) 63 | .padding(.horizontal, 40) 64 | CodeVerificationFieldView(code: $verificationCode) 65 | .disabled(isLoading) 66 | .focused($isTextFieldFocused) 67 | Spacer() 68 | Divider() 69 | HStack { 70 | Spacer() 71 | Button(action: { 72 | didPressSecondaryButton() 73 | }, label: { 74 | secondaryButtonLabel 75 | }) 76 | .disabled(isLoading) 77 | .keyboardShortcut(.cancelAction) 78 | ZStack { 79 | Button(action: { 80 | didPressMainButton() 81 | }, label: { 82 | mainButtonLabel 83 | }) 84 | .disabled(verificationCode.count < 6) 85 | .hiddenConditionally(isHidden: isLoading) 86 | .keyboardShortcut(.defaultAction) 87 | ProgressView() 88 | .progressViewStyle(.circular) 89 | .controlSize(.small) 90 | .hiddenConditionally(isHidden: !isLoading) 91 | } 92 | .padding(.leading, 6) 93 | } 94 | .padding(EdgeInsets(top: 8, leading: 16, bottom: 16, trailing: 16)) 95 | } 96 | .alert("code.verification.alert.code.error.title", isPresented: $showCodeError) { 97 | Button("code.verification.alert.code.error.action") { 98 | Task { @MainActor in 99 | self.isLoading = false 100 | } 101 | } 102 | } message: { 103 | Text("code.verification.alert.code.error.message") 104 | } 105 | .alert("code.verification.alert.connection.error.title", isPresented: $showConnectionError) { 106 | Button("code.verification.alert.connection.error.action") { 107 | action(previousPage) 108 | } 109 | } message: { 110 | Text("code.verification.alert.connection.error.message") 111 | } 112 | .onReceive(migrationController.$connectionState, perform: { newState in 113 | switch newState { 114 | case .setup: 115 | break 116 | case .waiting: 117 | isLoading = true 118 | case .preparing: 119 | isLoading = true 120 | case .ready: 121 | action(nextPage) 122 | case .failed, .cancelled: 123 | showCodeError.toggle() 124 | @unknown default: 125 | break 126 | } 127 | }) 128 | .onAppear { 129 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 130 | isTextFieldFocused = true 131 | } 132 | } 133 | } 134 | 135 | var mainButtonLabel: some View { 136 | Text("browser.page.button.main.label") 137 | .padding(4) 138 | } 139 | 140 | var secondaryButtonLabel: some View { 141 | Text("browser.page.button.secondary.label") 142 | .padding(4) 143 | } 144 | 145 | // MARK: - Private Methods 146 | 147 | private func didPressMainButton() { 148 | self.isLoading = true 149 | migrationController.connect(to: migrationController.selectedBrowserResult, withPasscode: verificationCode) { success in 150 | if !success { 151 | self.showConnectionError = true 152 | } 153 | } 154 | } 155 | 156 | private func didPressSecondaryButton() { 157 | migrationController.selectedBrowserResult = nil 158 | action(previousPage) 159 | } 160 | } 161 | 162 | #Preview { 163 | CodeVerificationView(action: { _ in }) 164 | } 165 | -------------------------------------------------------------------------------- /migrator/Views/Common/CodeVerificationFieldView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodeVerificationFieldView.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 15/03/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | import Combine 12 | 13 | /// This view is used to display a text field that accepts a six-char code. 14 | struct CodeVerificationFieldView: View { 15 | 16 | // MARK: - Enums 17 | 18 | /// An enum that represents the focus state of an element in the view. 19 | enum ElementFocusState: Hashable { 20 | case char(Int) 21 | } 22 | 23 | // MARK: - Focus State Variables 24 | 25 | /// The focus state of the view's elements. 26 | @FocusState private var charFocusState: ElementFocusState? 27 | 28 | // MARK: - Binded Variables 29 | 30 | /// The binded variable that stores the code entered by the user. 31 | @Binding private var code: String 32 | 33 | // MARK: - State Viariables 34 | 35 | /// The state variable that stores the individual characters of the code. 36 | @State private var chars: [String] 37 | 38 | // MARK: - Private Variables 39 | 40 | /// A boolean value that indicates whether the view should be displayed in read-only mode. 41 | private var readOnly: Bool 42 | 43 | // MARK: - Initializers 44 | 45 | /// Initializes the `CodeVerificationFieldView` with the given parameters. 46 | /// - Parameters: 47 | /// - code: The binded variable that stores the code entered by the user. 48 | /// - viewOnly: A boolean value that indicates whether the view should be displayed in read-only mode. 49 | init(code: Binding, viewOnly: Bool = false) { 50 | self._code = code 51 | self._chars = State(initialValue: Array(repeating: "", count: 6)) 52 | self.readOnly = viewOnly 53 | } 54 | 55 | // MARK: - Views 56 | 57 | var body: some View { 58 | HStack(spacing: 8) { 59 | ForEach(0..<6, id: \.self) { index in 60 | if readOnly { 61 | Text(chars[index]) 62 | .textFieldStyle(.plain) 63 | .multilineTextAlignment(.center) 64 | .frame(width: 43, height: 60) 65 | .font(.system(size: 36, weight: .semibold)) 66 | .background( 67 | Color("bigButtonUnselected") 68 | .clipShape(RoundedRectangle(cornerRadius: 16)) 69 | .shadow(color: Color(red: 0, green: 0, blue: 0, opacity: 0.15), radius: 6, x: 0, y: 0) 70 | ) 71 | } else { 72 | TextField("", text: $chars[index]) 73 | .textFieldStyle(.plain) 74 | .multilineTextAlignment(.center) 75 | .frame(width: 43, height: 60) 76 | .font(.system(size: 36, weight: .semibold)) 77 | .focused($charFocusState, equals: ElementFocusState.char(index)) 78 | .background(Color("bigButtonUnselected") 79 | .clipShape(RoundedRectangle(cornerRadius: 16)) 80 | .shadow(color: Color(red: 0, green: 0, blue: 0, opacity: 0.15), radius: 6, x: 0, y: 0)) 81 | .onChange(of: chars[index]) { newVal in 82 | if newVal.count == 1 { 83 | if index < 6 - 1 { 84 | charFocusState = ElementFocusState.char(index + 1) 85 | } else { 86 | charFocusState = nil 87 | } 88 | } else if newVal.count == 6 { 89 | code = newVal 90 | updateElements() 91 | charFocusState = ElementFocusState.char(6 - 1) 92 | } else if newVal.isEmpty { 93 | if index > 0 { 94 | charFocusState = ElementFocusState.char(index - 1) 95 | } 96 | } 97 | code = chars.joined() 98 | } 99 | .onTapGesture { 100 | charFocusState = ElementFocusState.char(index) 101 | } 102 | .onReceive(Just(chars[index])) { _ in 103 | if chars[index].count > 1 { 104 | self.chars[index] = String(chars[index].prefix(1)) 105 | } else { 106 | self.chars[index] = self.chars[index].uppercased() 107 | } 108 | } 109 | } 110 | } 111 | } 112 | .onAppear { 113 | updateElements() 114 | } 115 | } 116 | 117 | // MARK: - Private Methods 118 | 119 | /// Update the single elements of the code with their updated value, starting from the entire code. 120 | private func updateElements() { 121 | let tmpArray = Array(code.prefix(6)) 122 | for (index, char) in tmpArray.enumerated() { 123 | chars[index] = String(char) 124 | } 125 | } 126 | } 127 | 128 | #Preview { 129 | CodeVerificationFieldView(code: .constant("543216"), viewOnly: false) 130 | } 131 | -------------------------------------------------------------------------------- /migrator/Views/Common/DeviceListRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceListRow.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 05/01/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | /// Struct representing a row in a device list view 13 | struct DeviceListRow: View { 14 | 15 | // MARK: - Constants 16 | 17 | /// Network device model associated with this row 18 | let result: Binding 19 | 20 | // MARK: - Views 21 | 22 | /// Body of the view 23 | var body: some View { 24 | HStack { 25 | Text(result.name.wrappedValue) 26 | Spacer() 27 | if result.interfaces.contains(where: { $0.wrappedValue == .thunderbolt }) { 28 | Image(systemName: "cable.connector.horizontal") 29 | .resizable() 30 | .frame(width: 19, height: 9) 31 | } else if result.interfaces.contains(where: { $0.wrappedValue == .wifi }) { 32 | Image(systemName: "wifi.circle") 33 | .resizable() 34 | .frame(width: 18, height: 18) 35 | } else { 36 | Image(systemName: "antenna.radiowaves.left.and.right.circle") 37 | .resizable() 38 | .frame(width: 18, height: 18) 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /migrator/Views/Common/MigratorFileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MigratorFileView.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 15/02/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | /// Struct that define a view representing a migrator file. 13 | struct MigratorFileView: View { 14 | 15 | // MARK: - Variables 16 | 17 | /// The file that this view will represent. 18 | @Binding var file: MigratorFile 19 | /// Flag that enable descriptive File name. 20 | var needsDescriptiveLabel: Bool = false 21 | /// Flag that make directories clickable. 22 | var allowDirectoryOverview: Bool = true 23 | /// Flag that make file size visible or not. 24 | var showFileSize: Bool = false 25 | /// 26 | var showSelectionToggle: Bool = false 27 | 28 | // MARK: - State Variable 29 | 30 | /// State variable to show/hide popover to show content of the directory. 31 | @State private var showContent: Bool = false 32 | /// State variable used to show/hide hidden files in the directory content popover. 33 | @State private var showHiddenFiles: Bool = false 34 | /// State variable used to show/hide file size in order to wait it to be calculated before. 35 | @State private var fileSizeAvailable: Bool = false 36 | /// State variable used to track file selection. 37 | @State private var isSelected: Bool = false 38 | 39 | // MARK: - Private Computed Variables 40 | 41 | /// Embellished file name. 42 | private var descriptiveLabel: String { 43 | guard needsDescriptiveLabel else { return file.name } 44 | switch file.type { 45 | case .directory: 46 | return "\(file.name) Folder" 47 | default: 48 | return "\(file.name)" 49 | } 50 | } 51 | 52 | // MARK: - Views 53 | 54 | var body: some View { 55 | HStack { 56 | Image(nsImage: NSWorkspace.shared.icon(forFile: file.url.fullURL().relativePath)) 57 | .resizable() 58 | .frame(width: 20, height: 20) 59 | if file.type == .directory && !file.childs.isEmpty && allowDirectoryOverview { 60 | Button(action: { 61 | showContent.toggle() 62 | }, label: { 63 | Text(descriptiveLabel) 64 | .frame(maxWidth: 300) 65 | .lineLimit(1) 66 | .fixedSize() 67 | }) 68 | .buttonStyle(.link) 69 | .popover(isPresented: $showContent, arrowEdge: .trailing, content: { 70 | VStack { 71 | HStack { 72 | Toggle(isOn: $showHiddenFiles, label: { }) 73 | .controlSize(.mini) 74 | .toggleStyle(.switch) 75 | Text(String(format: "migration.setup.directory.content.hidden.label".localized, showHiddenFiles ? "migration.setup.directory.content.hidden.label.hide".localized : "migration.setup.directory.content.hidden.label.show".localized)) 76 | .lineLimit(1) 77 | Spacer(minLength: 32) 78 | Button(action: { 79 | NSWorkspace.shared.open(file.url.fullURL()) 80 | }, label: { 81 | HStack { 82 | Text("migration.setup.page.directory.popover.top.label") 83 | Image(systemName: "arrow.up.right.square") 84 | } 85 | }) 86 | .buttonStyle(.link) 87 | .fixedSize() 88 | } 89 | ScrollView { 90 | ForEach($file.childs) { child in 91 | if child.isHidden.wrappedValue && !showHiddenFiles { 92 | EmptyView() 93 | } else { 94 | MigratorFileView(file: child, allowDirectoryOverview: false, showFileSize: true) 95 | } 96 | } 97 | .padding(.trailing, 16) 98 | } 99 | } 100 | .frame(maxWidth: 500, maxHeight: 400) 101 | .padding([.leading, .vertical]) 102 | .padding(.trailing, 8) 103 | }) 104 | } else { 105 | Text(descriptiveLabel) 106 | .frame(maxWidth: 300) 107 | .lineLimit(1) 108 | .fixedSize() 109 | } 110 | Spacer() 111 | if showFileSize { 112 | if fileSizeAvailable { 113 | Text(file.fileSize.fileSizeToFormattedString) 114 | .lineLimit(1) 115 | .fixedSize() 116 | } else { 117 | ProgressView() 118 | .progressViewStyle(.circular) 119 | .controlSize(.mini) 120 | .padding(.horizontal, 8) 121 | } 122 | } 123 | if showSelectionToggle { 124 | Toggle(isOn: $isSelected.onUpdate { 125 | self.$file.isSelected.wrappedValue = self.isSelected 126 | }, label: {}) 127 | } 128 | } 129 | .opacity(file.isHidden ? 0.7 : 1) 130 | .onReceive(file.$fileSize) { size in 131 | guard size != -1 && showFileSize else { return } 132 | Task { @MainActor in 133 | self.fileSizeAvailable = true 134 | } 135 | } 136 | .onReceive(file.$isSelected) { newValue in 137 | guard self.isSelected != newValue else { return } 138 | self.isSelected = newValue 139 | } 140 | .onAppear { 141 | self.isSelected = file.isSelected 142 | } 143 | } 144 | } 145 | 146 | #Preview { 147 | MigratorFileView(file: .constant(MigratorFile(with: FileManager.default.homeDirectoryForCurrentUser))) 148 | .frame(width: 400, height: 50) 149 | } 150 | -------------------------------------------------------------------------------- /migrator/Views/Common/NoBackgroundScroller.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoBackgroundScroller.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 05/01/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import Cocoa 11 | 12 | /// Custom NSScroller subclass to remove the background and change alpha on mouse events 13 | class NoBackgroundScroller: NSScroller { 14 | 15 | /// Override draw method to only draw the knob 16 | override func draw(_ dirtyRect: NSRect) { 17 | self.drawKnob() 18 | } 19 | 20 | /// Override mouseEntered method to animate alpha value on mouse enter 21 | override func mouseEntered(with event: NSEvent) { 22 | NSAnimationContext.runAnimationGroup { (context) in 23 | context.duration = 0.1 24 | self.animator().alphaValue = 0.85 25 | } 26 | } 27 | 28 | /// Override mouseExited method to animate alpha value on mouse exit 29 | override func mouseExited(with event: NSEvent) { 30 | NSAnimationContext.runAnimationGroup { (context) in 31 | context.duration = 0.15 32 | self.animator().alphaValue = 0.35 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /migrator/Views/Common/WindowAccessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowAccessor.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 08/08/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | /// Used to access the NSWindow item. 13 | struct WindowAccessor: NSViewRepresentable { 14 | 15 | // MARK: - Environment Objects 16 | 17 | @EnvironmentObject private var appDelegate: AppDelegate 18 | 19 | // MARK: - Binding Variables 20 | 21 | @Binding var window: NSWindow? 22 | 23 | // MARK: - NSViewRepresentable methods implementation 24 | 25 | func makeNSView(context: Context) -> NSView { 26 | let view = NSView() 27 | Task { @MainActor in 28 | self.window = view.window 29 | self.window?.delegate = self.appDelegate 30 | } 31 | return view 32 | } 33 | 34 | func updateNSView(_ nsView: NSView, context: Context) {} 35 | } 36 | -------------------------------------------------------------------------------- /migrator/Views/JamfReconView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JamfReconView.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 22/08/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | /// Not used anymore 13 | /// Struct representing the page that guide the user to run an inventory update. 14 | struct JamfReconView: View { 15 | 16 | // MARK: - Private Constants 17 | 18 | /// Logger instance. 19 | private let logger: MLogger = MLogger.main 20 | 21 | // MARK: - Constants 22 | 23 | /// Closure to execute when an action requires navigation to a different page. 24 | let action: (MigratorPage) -> Void 25 | 26 | // MARK: - State Variables 27 | 28 | /// Boolean variable to track if the recon operation is running. 29 | @State private var runningRecon: Bool = false 30 | /// Boolean variable to track whenever the recon operation encounter an error. 31 | @State private var encounteredError: Bool = false 32 | 33 | // MARK: - Views 34 | 35 | var body: some View { 36 | VStack { 37 | Image("icon") 38 | .resizable() 39 | .frame(width: 86, height: 86) 40 | .padding(.top, 55) 41 | .padding(.bottom, 8) 42 | Text("recon.page.title.label") 43 | .multilineTextAlignment(.center) 44 | .font(.system(size: 27, weight: .bold)) 45 | .padding(.bottom, 8) 46 | Text(String(format: "recon.page.body.label".localized, AppContext.orgName, "recon.page.main.button.run.label".localized)) 47 | .multilineTextAlignment(.center) 48 | .padding(.bottom) 49 | .padding(.horizontal, 40) 50 | Spacer() 51 | Divider() 52 | HStack { 53 | if runningRecon { 54 | ProgressView() 55 | .controlSize(.small) 56 | Text("recon.page.informational.bottom.ongoing.label") 57 | .padding(.leading, 8) 58 | } 59 | Spacer() 60 | Button(action: { 61 | Task { 62 | await didPressMainButton() 63 | } 64 | }, label: { 65 | mainButtonLabel 66 | }) 67 | .buttonStyle(.bordered) 68 | .keyboardShortcut(.defaultAction) 69 | .disabled(runningRecon) 70 | .padding(.leading, 6) 71 | } 72 | .padding(EdgeInsets(top: 8, leading: 16, bottom: 16, trailing: 16)) 73 | } 74 | .alert("recon.page.alert.error.title", isPresented: $encounteredError, actions: { }, message: { 75 | Text("recon.page.alert.error.message") 76 | }) 77 | .onAppear { 78 | Utils.makeWindowFloating() 79 | } 80 | } 81 | 82 | var mainButtonLabel: some View { 83 | Text("recon.page.main.button.run.label") 84 | .padding(4) 85 | } 86 | 87 | // MARK: - Private Methods 88 | 89 | private func didPressMainButton() async { 90 | await MainActor.run { 91 | runningRecon.toggle() 92 | } 93 | logger.log("jamfReconView.runJamfRecon: Starting Jamf Inventory Update", type: .default) 94 | var errors: NSDictionary? 95 | await withCheckedContinuation { continuation in 96 | Task.detached(priority: .background) { 97 | _ = NSAppleScript(source: "do shell script \"/usr/local/bin/jamf recon\" with administrator privileges")?.executeAndReturnError(&errors) 98 | continuation.resume() 99 | } 100 | } 101 | if errors == nil { 102 | logger.log("jamfReconView.runJamfRecon: Jamf Inventory Update completed", type: .default) 103 | await Utils.removeLaunchAgent() 104 | AppContext.isPostRebootPhase = false 105 | NSSound(named: .init("Funk"))?.play() 106 | action(.final) 107 | } else { 108 | logger.log("jamfReconView.runJamfRecon: Jamf Inventory Update completed with error: \(errors.debugDescription)", type: .default) 109 | await MainActor.run { 110 | encounteredError.toggle() 111 | } 112 | } 113 | await MainActor.run { 114 | runningRecon.toggle() 115 | } 116 | } 117 | } 118 | 119 | #Preview { 120 | JamfReconView(action: { _ in }) 121 | .frame(width: 812, height: 600) 122 | } 123 | -------------------------------------------------------------------------------- /migrator/Views/MainView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainView.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 16/11/2023. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | /// Scruct representing the MainView of the App. 13 | struct MainView: View { 14 | 15 | // MARK: - State Variables 16 | 17 | /// State variable to keep track of the current page in the view 18 | @State private var currentPage: MigratorPage = .welcome 19 | 20 | // MARK: - Initializers 21 | 22 | init() { 23 | if AppContext.isPostRebootPhase { 24 | _currentPage = State(initialValue: .reboot.next()) 25 | } 26 | } 27 | 28 | // MARK: - Views 29 | 30 | var body: some View { 31 | currentPage.view(action: { nextPage in 32 | currentPage = nextPage 33 | }) 34 | .frame(width: 812, height: 600) 35 | } 36 | } 37 | 38 | #Preview { 39 | MainView() 40 | } 41 | -------------------------------------------------------------------------------- /migrator/Views/Migration/MigrationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MigrationView.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 16/11/2023. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | /// Struct representing the Migration view. 13 | struct MigrationView: View { 14 | 15 | // MARK: - Constants 16 | 17 | /// Closure to execute when an action requires navigation to a different page. 18 | let action: (MigratorPage) -> Void 19 | 20 | // MARK: - Environment Variables 21 | 22 | @Environment(\.colorScheme) var colorScheme 23 | @EnvironmentObject private var appDelegate: AppDelegate 24 | 25 | // MARK: - State Variables 26 | 27 | /// State variable to track warning popover appearance. 28 | @State private var showWarningPopover: Bool = true 29 | 30 | // MARK: - Observed Variables 31 | 32 | /// Observable view model object to handle data and logic for the server view 33 | @ObservedObject var viewModel: MigrationViewModel = MigrationViewModel() 34 | 35 | // MARK: - Views 36 | 37 | var body: some View { 38 | VStack { 39 | Image("icon") 40 | .resizable() 41 | .frame(width: 86, height: 86) 42 | .padding(.top, 55) 43 | .padding(.bottom, 8) 44 | Text(viewModel.migrationProgress == 1 ? "migration.page.title.complete.label".localized : "migration.page.title.ongoing.label".localized) 45 | .multilineTextAlignment(.center) 46 | .font(.system(size: 27, weight: .bold)) 47 | .padding(.bottom, 8) 48 | Text(viewModel.migrationProgress == 1 ? "migration.page.body.source.complete.label".localized : "migration.page.body.ongoing.label".localized) 49 | .multilineTextAlignment(.center) 50 | .padding(.bottom) 51 | .padding(.horizontal, 40) 52 | Image("new_mac") 53 | .padding(.vertical, 4) 54 | Group { 55 | Text(viewModel.migrationProgress == 1 ? "migration.page.progressbar.top.complete.label".localized : "migration.page.progressbar.top.ongoing.label".localized) + Text(viewModel.migrationController.hostName).fontWeight(.bold) + Text(viewModel.usedInterface) 56 | } 57 | .padding(.vertical, 4) 58 | VStack { 59 | ProgressView(value: viewModel.migrationProgress == 0 ? nil : viewModel.migrationProgress) 60 | .progressViewStyle(.linear) 61 | .controlSize(.regular) 62 | HStack { 63 | Text(viewModel.estimatedTimeLeft) 64 | .font(.callout) 65 | Spacer() 66 | Text(viewModel.percentageCompleted) 67 | .font(.callout) 68 | } 69 | } 70 | .padding(.horizontal, 176) 71 | Spacer() 72 | Divider() 73 | HStack { 74 | if !viewModel.deviceIsConnectedToPower { 75 | Button { 76 | showWarningPopover.toggle() 77 | } label: { 78 | Image(systemName: "exclamationmark") 79 | } 80 | .clipShape(Circle()) 81 | .popover(isPresented: $showWarningPopover, arrowEdge: .bottom, content: { 82 | Text("migration.page.warning.button.popover.text") 83 | .padding() 84 | }) 85 | } 86 | Spacer() 87 | Button(action: { 88 | appDelegate.quit() 89 | }, label: { 90 | Text("migration.page.main.button.label") 91 | .padding(4) 92 | }) 93 | .hiddenConditionally(isHidden: viewModel.migrationProgress < 1) 94 | .keyboardShortcut(.defaultAction) 95 | } 96 | .padding(EdgeInsets(top: 8, leading: 16, bottom: 16, trailing: 16)) 97 | } 98 | } 99 | } 100 | 101 | #Preview { 102 | MigrationView(action: { _ in }) 103 | .frame(width: 812, height: 600) 104 | } 105 | -------------------------------------------------------------------------------- /migrator/Views/MigrationSetup/AdvancedSelectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdvancedSelectionView.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 15/02/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | import Combine 12 | 13 | /// Struct that define the view representing the advanced selection of files/apps/preferences for the migration. 14 | struct AdavancedSelectionView: View { 15 | 16 | // MARK: - Enums 17 | 18 | /// Support Enum that define the different sections available. 19 | enum AdvancedSelectionSection { 20 | case files 21 | case applications 22 | case preferences 23 | 24 | /// Title of the section, to be used in the segmented picker. 25 | var title: String { 26 | switch self { 27 | case .files: 28 | return "migration.option.advanced.segment.files".localized 29 | case .applications: 30 | return "migration.option.advanced.segment.applications".localized 31 | case .preferences: 32 | return "migration.option.advanced.segment.preferences".localized 33 | } 34 | } 35 | } 36 | 37 | // MARK: - Binded Variables 38 | 39 | /// Binded list of the available files for this selection. 40 | @Binding private(set) var migrationOption: MigrationOption 41 | 42 | // MARK: - State Variables 43 | 44 | /// State variable to track picker selection. 45 | @State private var advancedSelectionSegment: AdvancedSelectionSection = .files 46 | /// State variable to track the visibility of hidden files. 47 | @State private var showHiddenFiles: Bool = false 48 | /// State variable that counts the number of selected files. 49 | @State private var selectedFilesNum: Int = 0 50 | /// State variable that counts the number of selected files. 51 | @State private var selectedAppsNum: Int = 0 52 | 53 | // MARK: - Private Variables 54 | 55 | /// Collection of cancellable subscriptions to manage memory and avoid retain cycles. 56 | lazy private var cancellables = Set() 57 | 58 | // MARK: - Initializers 59 | 60 | init(migrationOption: Binding) { 61 | self._migrationOption = migrationOption 62 | } 63 | 64 | // MARK: - Views 65 | 66 | var body: some View { 67 | VStack(spacing: 0) { 68 | Picker(selection: $advancedSelectionSegment) { 69 | Text(AdvancedSelectionSection.files.title.localized + (selectedFilesNum > 0 ? " (\(migrationOption.selectedFiles))" : "")) 70 | .tag(AdvancedSelectionSection.files) 71 | Text(AdvancedSelectionSection.applications.title.localized + (selectedAppsNum > 0 ? " (\(migrationOption.selectedApps))" : "")) 72 | .tag(AdvancedSelectionSection.applications) 73 | } label: {} 74 | .pickerStyle(.segmented) 75 | .background { 76 | Color("discoveryViewBackground").clipShape(RoundedRectangle(cornerRadius: 6)) 77 | } 78 | .frame(width: 479, height: 22) 79 | .padding(.top, -11) 80 | Spacer() 81 | switch advancedSelectionSegment { 82 | case .files: 83 | HStack { 84 | Spacer() 85 | if #available(macOS 13.0, *) { 86 | Text("migration.setup.page.advanced.select.all") 87 | Toggle(sources: $migrationOption.migrationFileList, isOn: \.isSelected) { } 88 | .padding(.trailing, 16) 89 | } 90 | } 91 | List($migrationOption.migrationFileList) { file in 92 | if file.isHidden.wrappedValue && !showHiddenFiles { 93 | EmptyView() 94 | } else { 95 | HStack { 96 | MigratorFileView(file: file, showFileSize: true, showSelectionToggle: true) 97 | } 98 | } 99 | } 100 | case .applications: 101 | HStack { 102 | Spacer() 103 | if #available(macOS 13.0, *) { 104 | Text("migration.setup.page.advanced.select.all") 105 | Toggle(sources: $migrationOption.migrationAppList, isOn: \.isSelected) { } 106 | .padding(.trailing, 16) 107 | } 108 | } 109 | List($migrationOption.migrationAppList) { file in 110 | if file.isHidden.wrappedValue && !showHiddenFiles { 111 | EmptyView() 112 | } else { 113 | HStack { 114 | MigratorFileView(file: file, showFileSize: true, showSelectionToggle: true) 115 | } 116 | } 117 | } 118 | case .preferences: 119 | EmptyView() 120 | } 121 | HStack { 122 | Toggle(isOn: $showHiddenFiles, label: { }) 123 | .controlSize(.mini) 124 | .toggleStyle(.switch) 125 | Text(String(format: "migration.setup.directory.content.hidden.label".localized, showHiddenFiles ? "migration.setup.directory.content.hidden.label.hide".localized : "migration.setup.directory.content.hidden.label.show".localized)) 126 | Spacer() 127 | } 128 | .padding() 129 | } 130 | .padding(.horizontal, 8) 131 | .onReceive(self.migrationOption.$selectedFiles, perform: { number in 132 | self.selectedFilesNum = number 133 | }) 134 | .onReceive(self.migrationOption.$selectedApps, perform: { number in 135 | self.selectedAppsNum = number 136 | }) 137 | } 138 | } 139 | 140 | private let ASVPreviewOption = MigrationOption(type: .advanced) 141 | #Preview { 142 | AdavancedSelectionView(migrationOption: .constant(ASVPreviewOption)) 143 | .frame(width: 812, height: 600) 144 | .task { 145 | await ASVPreviewOption.loadFiles() 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /migrator/Views/MigrationSetup/MigrationOptionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MigrationOptionView.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 31/01/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | /// Struct representing a migration option. 13 | struct MigrationOptionView: View { 14 | 15 | // MARK: - Variables 16 | 17 | /// The migration option to be displayed. 18 | @ObservedObject var option: MigrationOption 19 | 20 | // MARK: - Views 21 | 22 | var body: some View { 23 | VStack(alignment: .leading) { 24 | HStack { 25 | Text(option.name) 26 | .font(.title2) 27 | Spacer() 28 | if !option.isFinalSize { 29 | ProgressView() 30 | .progressViewStyle(.circular) 31 | .controlSize(.small) 32 | } 33 | if option.size > 0 { 34 | Text(option.size.fileSizeToFormattedString) 35 | } 36 | } 37 | .padding(.top, 8) 38 | HStack { 39 | VStack(alignment: .leading) { 40 | ForEach(option.migrationFileList) { file in 41 | MigratorFileView(file: .constant(file), needsDescriptiveLabel: true, showFileSize: false) 42 | } 43 | ForEach(option.migrationAppList) { file in 44 | MigratorFileView(file: .constant(file), needsDescriptiveLabel: true, showFileSize: false) 45 | } 46 | } 47 | .padding(.horizontal, 8) 48 | } 49 | Spacer() 50 | Divider() 51 | .padding(.leading, -36) 52 | .padding(.trailing, -7) 53 | } 54 | } 55 | } 56 | 57 | private let MOVPreviewOption = MigrationOption(type: .complete) 58 | #Preview { 59 | MigrationOptionView(option: MOVPreviewOption) 60 | .frame(width: 400, height: 200) 61 | .task { 62 | await MOVPreviewOption.loadFiles() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /migrator/Views/RebootView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RebootView.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 22/08/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | /// Struct representing the page that ask the user to restart the device. 13 | struct RebootView: View { 14 | 15 | // MARK: - Constants 16 | 17 | /// Closure to execute when an action requires navigation to a different page. 18 | let action: (MigratorPage) -> Void 19 | let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() 20 | 21 | // MARK: - State Variables 22 | 23 | /// State variable that tracks the time left to force the device reboot. 24 | @State private var timeLeft: Int = 120 25 | 26 | // MARK: - Views 27 | 28 | var body: some View { 29 | VStack { 30 | Image("icon") 31 | .resizable() 32 | .frame(width: 86, height: 86) 33 | .padding(.top, 55) 34 | .padding(.bottom, 8) 35 | Text("reboot.page.title.label") 36 | .multilineTextAlignment(.center) 37 | .font(.system(size: 27, weight: .bold)) 38 | .padding(.bottom, 8) 39 | Text("reboot.page.body.label") 40 | .multilineTextAlignment(.center) 41 | .padding(.bottom) 42 | .padding(.horizontal, 40) 43 | Spacer() 44 | Divider() 45 | HStack { 46 | Text(String(format: "reboot.page.bottom.timer.label".localized, timeLeft.timeFormattedString)) 47 | .font(.title3) 48 | .bold() 49 | Spacer() 50 | Button(action: { 51 | didPressMainButton() 52 | }, label: { 53 | mainButtonLabel 54 | }) 55 | .buttonStyle(.bordered) 56 | .keyboardShortcut(.defaultAction) 57 | .padding(.leading, 6) 58 | } 59 | .padding(EdgeInsets(top: 8, leading: 16, bottom: 16, trailing: 16)) 60 | } 61 | .onReceive(timer) { _ in 62 | guard timeLeft >= 1 else { 63 | self.timer.upstream.connect().cancel() 64 | didPressMainButton() 65 | return 66 | } 67 | timeLeft -= 1 68 | } 69 | .onAppear { 70 | Utils.makeWindowFloating() 71 | } 72 | } 73 | 74 | var mainButtonLabel: some View { 75 | Text("reboot.page.main.button.label") 76 | .padding(4) 77 | } 78 | 79 | // MARK: - Private Methods 80 | 81 | private func didPressMainButton() { 82 | AppContext.isPostRebootPhase = true 83 | Task { 84 | await Utils.installLaunchAgent() 85 | Utils.rebootMac() 86 | } 87 | } 88 | } 89 | 90 | #Preview { 91 | RebootView(action: { _ in }) 92 | .frame(width: 812, height: 600) 93 | } 94 | -------------------------------------------------------------------------------- /migrator/Views/Server/ServerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiscoveryView.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 14/11/2023. 6 | // Copyright © 2023 IBM Inc. All rights reserved 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | import Network 12 | 13 | /// Struct representing the page to visualize the Server behaviour. 14 | struct ServerView: View { 15 | 16 | // MARK: - Environment Variables 17 | 18 | @Environment(\.colorScheme) var colorScheme 19 | @EnvironmentObject private var appDelegate: AppDelegate 20 | 21 | // MARK: - Constants 22 | 23 | /// Closure to execute when an action requires navigation 24 | let action: (MigratorPage) -> Void 25 | /// The previous page to return to, defaulted to the welcome page 26 | let previousPage: MigratorPage = .welcome 27 | 28 | // MARK: - Observable Variables 29 | 30 | /// Observable object to control and observe migration status 31 | @ObservedObject var migrationController: MigrationController = MigrationController.shared 32 | /// Observable view model object to handle data and logic for the server view 33 | @ObservedObject var viewModel: ServerViewModel = ServerViewModel() 34 | 35 | // MARK: - State Variables 36 | 37 | /// State variable to track warning popover appearance. 38 | @State private var showWarningPopover: Bool = true 39 | 40 | // MARK: - Initializers 41 | 42 | /// Custom initializer to set up the view with a navigation action and start the listening process. 43 | /// - Parameter action: the navigation action. 44 | init(action: @escaping (MigratorPage) -> Void) { 45 | self.action = action 46 | } 47 | 48 | // MARK: - Views 49 | 50 | var body: some View { 51 | VStack { 52 | Image("icon") 53 | .resizable() 54 | .frame(width: 86, height: 86) 55 | .padding(.top, 55) 56 | .padding(.bottom, 8) 57 | Text(viewModel.connectionEstablished ? (viewModel.migrationProgress > 0 ? (viewModel.migrationProgress == 1 ? "server.page.title.migration.complete.label" : "server.page.title.migration.ongoing.label") : "server.page.connected.title") : "server.page.title") 58 | .multilineTextAlignment(.center) 59 | .font(.system(size: 27, weight: .bold)) 60 | .padding(.bottom, 8) 61 | Text(viewModel.connectionEstablished ? (viewModel.migrationProgress > 0 ? (viewModel.migrationProgress == 1 ? "server.page.body.migration.complete.label" : "server.page.body.ongoing.label") : "server.page.connected.subtitle") : "server.page.subtitle") 62 | .multilineTextAlignment(.center) 63 | .padding(.bottom) 64 | .padding(.horizontal, 40) 65 | Image(viewModel.connectionEstablished ? "old_mac" : "new_mac") 66 | if viewModel.connectionEstablished { 67 | Group { 68 | Text(viewModel.migrationProgress == 1 ? "migration.page.progressbar.top.complete.label".localized : "migration.page.progressbar.top.ongoing.label".localized) + Text(migrationController.hostName).fontWeight(.bold) + Text(viewModel.usedInterface) 69 | } 70 | .padding(.vertical, 4) 71 | } else { 72 | Text(Host.current().localizedName ?? "server.page.default.device.name") 73 | .font(.headline) 74 | .padding(.vertical, 4) 75 | } 76 | VStack { 77 | if viewModel.connectionEstablished { 78 | if viewModel.migrationProgress == 0 { 79 | ProgressView() 80 | .progressViewStyle(.linear) 81 | .controlSize(.regular) 82 | } else { 83 | ProgressView(value: viewModel.migrationProgress) 84 | .progressViewStyle(.linear) 85 | .controlSize(.regular) 86 | 87 | } 88 | HStack { 89 | Spacer() 90 | Text("\(viewModel.percentageCompleted)") 91 | .font(.callout) 92 | } 93 | } else { 94 | VStack(spacing: 12) { 95 | ProgressView() 96 | .progressViewStyle(.circular) 97 | .controlSize(.small) 98 | Spacer() 99 | Text("server.page.pairing.code.label") 100 | .font(.title3) 101 | CodeVerificationFieldView(code: .constant(viewModel.randomCode), viewOnly: true) 102 | Spacer() 103 | } 104 | } 105 | } 106 | .padding(.horizontal, 176) 107 | Spacer() 108 | Divider() 109 | HStack { 110 | if !viewModel.deviceIsConnectedToPower { 111 | Button { 112 | showWarningPopover.toggle() 113 | } label: { 114 | Image(systemName: "exclamationmark") 115 | } 116 | .clipShape(Circle()) 117 | .popover(isPresented: $showWarningPopover, arrowEdge: .bottom, content: { 118 | Text("migration.page.warning.button.popover.text") 119 | .padding() 120 | }) 121 | } 122 | Spacer() 123 | if viewModel.connectionEstablished { 124 | Button(action: { 125 | action(.server.next()) 126 | }, label: { 127 | Text("server.page.button.main.continue.label") 128 | .padding(4) 129 | }) 130 | .disabled(viewModel.migrationProgress != 1) 131 | .keyboardShortcut(.defaultAction) 132 | } else { 133 | Button(action: { 134 | migrationController.stopServer() 135 | action(previousPage) 136 | }, label: { 137 | Text("server.page.button.main.label") 138 | .foregroundColor(Color.red) 139 | .padding(4) 140 | }) 141 | .keyboardShortcut(.cancelAction) 142 | } 143 | } 144 | .padding(EdgeInsets(top: 8, leading: 16, bottom: 16, trailing: 16)) 145 | } 146 | .alert("connection.error.alert.unrecoverable.title", isPresented: $viewModel.connectionInterrupted) { 147 | Button("connection.error.alert.unrecoverable.main.action.label") { 148 | Task { @MainActor in 149 | self.viewModel.resetMigration() 150 | self.action(.welcome) 151 | } 152 | } 153 | } message: { 154 | Text("connection.error.alert.unrecoverable.message") 155 | } 156 | } 157 | } 158 | 159 | #Preview { 160 | ServerView(action: {_ in }) 161 | .frame(width: 812, height: 600) 162 | } 163 | -------------------------------------------------------------------------------- /migrator/Views/Server/ServerViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServerViewModel.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 26/02/2024. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | import Combine 12 | 13 | /// ViewModel for the Server view. 14 | class ServerViewModel: ObservableObject { 15 | 16 | // MARK: - Variables 17 | 18 | var usedInterface: String { 19 | guard let currentPath = self.migrationController.connection?.connection.currentPath else { return "" } 20 | guard let currentInterface = currentPath.availableInterfaces.first(where: { currentPath.usesInterfaceType($0.type) }) else { return "" } 21 | switch currentInterface.type { 22 | case .wifi, .cellular: 23 | return " " + "migration.page.technology.wifi.label".localized 24 | case .wiredEthernet: 25 | return " " + "migration.page.technology.thunderbolt.label".localized 26 | default: 27 | return "" 28 | } 29 | } 30 | 31 | // MARK: - Observed Variables 32 | 33 | /// Observable object to control and observe migration status 34 | @ObservedObject private var migrationController: MigrationController = MigrationController.shared 35 | 36 | // MARK: - Published Variables 37 | 38 | /// Published variable that track if the device is connected to a power source. 39 | @Published var deviceIsConnectedToPower: Bool = true 40 | /// Indicates whether a connection to a migration client has been established. 41 | @Published var connectionEstablished: Bool = false 42 | /// Tracks unexpected connection interruptions. 43 | @Published var connectionInterrupted: Bool = false 44 | /// Track the progress of the migration. 45 | @Published var migrationProgress: Double = 0 46 | /// Published variable that track the percentage of completion of the migration. 47 | @Published var percentageCompleted: String = "server.page.progressbar.top.percentage.start.label".localized 48 | /// Random pairing code. 49 | @Published var randomCode: String = "" 50 | 51 | // MARK: - Private Variables 52 | 53 | /// Collection of cancellable subscriptions to manage memory and avoid retain cycles. 54 | private var cancellables = Set() 55 | /// Counts the number of bytes received from file trasfer messages. 56 | private var bytesReceived: Int = 1 57 | 58 | // MARK: - Initializers 59 | 60 | init() { 61 | self.deviceIsConnectedToPower = IOPSCopyExternalPowerAdapterDetails()?.takeRetainedValue() != nil 62 | self.randomCode = Utils.generateRandomCode(digits: 6) 63 | self.migrationController.startServer(withPasscode: randomCode) 64 | self.migrationController.$isConnected.sink { newValue in 65 | Task { @MainActor in 66 | if newValue { 67 | self.connectionEstablished = newValue 68 | self.connectionInterrupted = false 69 | // Stops the server to prevent additional connections once one is established. 70 | self.migrationController.stopServer() 71 | Utils.preventSleep() 72 | } else { 73 | self.connectionInterrupted = self.connectionEstablished 74 | } 75 | } 76 | }.store(in: &cancellables) 77 | self.migrationController.$connectionState.sink { newState in 78 | switch newState { 79 | default: 80 | return 81 | } 82 | }.store(in: &cancellables) 83 | self.migrationController.$bytesReceived.sink { bytesCount in 84 | Task { @MainActor in 85 | self.bytesReceived = bytesCount 86 | if self.migrationController.sizeOfMigration != 0 { 87 | self.migrationProgress = min(Double(self.bytesReceived)/Double(self.migrationController.sizeOfMigration), 0.99) 88 | self.percentageCompleted = "\(min(99, self.bytesReceived*100/self.migrationController.sizeOfMigration))%" 89 | } 90 | } 91 | }.store(in: &cancellables) 92 | self.migrationController.$isMigrationCompleted.sink { isCompleted in 93 | guard isCompleted else { return } 94 | Task { @MainActor in 95 | self.percentageCompleted = "100%" 96 | self.migrationProgress = 1 97 | self.cancellables.removeAll() 98 | Utils.makeWindowFloating() 99 | } 100 | }.store(in: &cancellables) 101 | NotificationCenter.default.addObserver(self, selector: #selector(devicePowerSourceDidUpdate), name: .devicePowerStatusChanged, object: nil) 102 | } 103 | 104 | func resetMigration() { 105 | self.migrationController.resetMigration() 106 | } 107 | 108 | // MARK: - Private Functions 109 | 110 | /// Handle the devicePowerStatusChanged notification. 111 | @objc 112 | private func devicePowerSourceDidUpdate(_ notification: Notification) { 113 | guard let newValue = notification.userInfo?["newValue"] as? Bool else { return } 114 | self.deviceIsConnectedToPower = newValue 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /migrator/Views/WelcomeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WelcomeView.swift 3 | // IBM Data Shift 4 | // 5 | // Created by Simone Martorelli on 16/11/2023. 6 | // © Copyright IBM Corp. 2023, 2024 7 | // SPDX-License-Identifier: Apache2.0 8 | // 9 | 10 | import SwiftUI 11 | 12 | /// Struct representing the WelcomeView page. 13 | struct WelcomeView: View { 14 | 15 | // MARK: - Environment Variables 16 | 17 | @Environment(\.colorScheme) var colorScheme 18 | 19 | // MARK: - Constants 20 | 21 | /// Closure to execute when an action is taken that requires navigation 22 | let action: (MigratorPage) -> Void 23 | 24 | // MARK: - Variables 25 | 26 | var appName: String { 27 | return Bundle.main.name 28 | } 29 | 30 | // MARK: - State Variables 31 | 32 | /// State to keep track of the next page to navigate to 33 | @State private var nextPage: MigratorPage = .welcome 34 | /// State to control the visibility of FDA error messages 35 | @State private var showFDAError: Bool = false 36 | /// State to control the visibility of Management error messages 37 | @State private var showManagementError: Bool = false 38 | 39 | // MARK: - Views 40 | 41 | var body: some View { 42 | VStack { 43 | Image("icon") 44 | .resizable() 45 | .frame(width: 86, height: 86) 46 | .padding(.top, 55) 47 | .padding(.bottom, 8) 48 | Text(String(format: "welcome.page.title".localized, Bundle.main.name)) 49 | .multilineTextAlignment(.center) 50 | .font(.system(size: 27, weight: .bold)) 51 | .padding(.bottom, 8) 52 | Text("welcome.page.subtitle") 53 | .multilineTextAlignment(.center) 54 | .padding(.bottom) 55 | .padding(.horizontal, 40) 56 | HStack(spacing: 86) { 57 | VStack { 58 | Button(action: { 59 | nextPage = .server 60 | }, label: { 61 | Image("new_mac") 62 | .frame(width: 120, height: 120) 63 | .background(content: { 64 | // Change background based on selection 65 | if nextPage == .server { 66 | LinearGradient.bigButtonSelected(colorScheme: colorScheme) 67 | .clipShape(RoundedRectangle(cornerRadius: 8)) 68 | } else { 69 | Color("bigButtonUnselected") 70 | .clipShape(RoundedRectangle(cornerRadius: 8)) 71 | } 72 | }) 73 | }) 74 | .buttonStyle(.plain) 75 | .shadow(color: Color(red: 0, green: 0, blue: 0, opacity: 0.3), radius: 2.5, x: 0, y: 0.5) 76 | .padding(.bottom, 6) 77 | Text("welcome.page.button.big.left.label") 78 | } 79 | VStack { 80 | Button(action: { 81 | nextPage = .browser 82 | }, label: { 83 | Image("old_mac") 84 | .frame(width: 120, height: 120) 85 | .background(content: { 86 | // Change background based on selection 87 | if nextPage == .browser { 88 | LinearGradient.bigButtonSelected(colorScheme: colorScheme) 89 | .clipShape(RoundedRectangle(cornerRadius: 8)) 90 | } else { 91 | Color("bigButtonUnselected") 92 | .clipShape(RoundedRectangle(cornerRadius: 8)) 93 | } 94 | }) 95 | }) 96 | .buttonStyle(.plain) 97 | .shadow(color: Color.black.opacity(0.3), radius: 2.5, x: 0, y: 0.5) 98 | .padding(.bottom, 6) 99 | Text("welcome.page.button.big.right.label") 100 | } 101 | } 102 | Spacer() 103 | Divider() 104 | HStack { 105 | Spacer() 106 | Button(action: { 107 | action(nextPage) 108 | }, label: { 109 | Text("welcome.page.button.main.label") 110 | .padding(4) 111 | }) 112 | .disabled(nextPage == .welcome) 113 | .keyboardShortcut(.defaultAction) 114 | } 115 | .padding(EdgeInsets(top: 8, leading: 16, bottom: 16, trailing: 16)) 116 | } 117 | .alert("welcome.page.fda.error.title", isPresented: $showFDAError, actions: { 118 | Button(action: { 119 | NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles")!) 120 | exit(0) 121 | }, label: { 122 | Text(String(format: "welcome.page.fda.error.first.action.title".localized, Utils.systemSettingsLabel)) 123 | }) 124 | Button { 125 | exit(0) 126 | } label: { 127 | Text("welcome.page.fda.error.second.action.title") 128 | } 129 | }, message: { 130 | Text(String(format: "welcome.page.fda.error.message".localized, appName, Utils.systemSettingsLabel, appName)) 131 | }) 132 | .alert(String(format: "welcome.page.management.error.title".localized, AppContext.orgName), isPresented: $showManagementError, actions: { 133 | Button(action: { 134 | NSWorkspace.shared.open(URL(string: AppContext.enrollmentRedirectionLink)!) 135 | exit(0) 136 | }, label: { 137 | Text("welcome.page.management.error.first.action.title") 138 | }) 139 | }, message: { 140 | Text(String(format: "welcome.page.management.error.message".localized, AppContext.orgName, AppContext.orgName)) 141 | }) 142 | .onAppear { 143 | // Trying to access a file unaccessible without Full Disk Access permissions as there isn't a way to ask for those permission with an API. This trick allow the app to be in the Full Disk Access app list. 144 | try? FileManager.default.copyItem(atPath: "/Library/Preferences/com.apple.TimeMachine.plist", toPath: "/private/tmp/com.apple.TimeMachine.plist") 145 | // If the app is not able to read at this path it means that it doesn't have FDA permissions so an alert is showed to ask the user to allow it. 146 | if !FileManager.default.isReadableFile(atPath: "/Library/Preferences/com.apple.TimeMachine.plist") { 147 | showFDAError.toggle() 148 | } 149 | if !AppContext.shouldSkipMDMCheck { 150 | switch DeviceManagementHelper.shared.state { 151 | case .unmanaged, .unknown, .managedByUnknownOrg: 152 | showManagementError.toggle() 153 | case .managed: 154 | break 155 | } 156 | } 157 | } 158 | } 159 | } 160 | 161 | #Preview { 162 | WelcomeView(action: {_ in }) 163 | .frame(width: 812, height: 600) 164 | } 165 | -------------------------------------------------------------------------------- /migrator/migrator.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /migratorTests/MigratorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MigratorTests.swift 3 | // migratorTests 4 | // 5 | // Created by Simone Martorelli on 14/11/2023. 6 | // 7 | 8 | import XCTest 9 | @testable import Mac_IBM_Shift 10 | 11 | final class MigratorTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | 29 | func testPerformanceExample() throws { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /migratorUITests/MigratorUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MigratorUITests.swift 3 | // migratorUITests 4 | // 5 | // Created by Simone Martorelli on 14/11/2023. 6 | // 7 | 8 | import XCTest 9 | 10 | final class MigratorUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /migratorUITests/MigratorUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MigratorUITestsLaunchTests.swift.swift 3 | // migratorUITests 4 | // 5 | // Created by Simone Martorelli on 14/11/2023. 6 | // 7 | 8 | import XCTest 9 | 10 | final class MigratorUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | --------------------------------------------------------------------------------