diff --git a/.eslintignore b/.eslintignore
index 2b6e224f..8a73bd95 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -46,6 +46,7 @@ dist
dll
main.js
main.js.map
+app/vendors
.idea
@@ -67,3 +68,24 @@ app/dll
.prettierrc
.stylelintrc
.eslintrc.json
+
+
+package-lock.json
+sentry.properties
+
+app/certs/*.pem
+certs/*
+todo.txt
+*yarn-error.log
+eslint-common-rules.txt
+
+build/entitlements.mas.plist
+*embedded.provisionprofile
+extras/*
+mtp-mock-files/*
+resources
+tmp/*
+*.prod.js
+*.js.map
+*.js.LICENSE.txt
+*.prod.js.LICENSE.txt
diff --git a/.eslintrc.js b/.eslintrc.js
index 8ab2a483..fe47baab 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,33 +1,42 @@
module.exports = {
- parser: 'babel-eslint',
+ parser: '@babel/eslint-parser',
parserOptions: {
sourceType: 'module',
- allowImportExportEverywhere: true
+ allowImportExportEverywhere: true,
},
env: {
browser: true,
- node: true
+ node: true,
},
plugins: ['import', 'promise', 'compat', 'react'],
- extends: ['airbnb', 'plugin:prettier/recommended', 'prettier/react'],
+ extends: ['airbnb', 'plugin:prettier/recommended'],
settings: {
react: {
- version: '16.7'
+ version: '16.7',
},
'import/resolver': {
webpack: {
- config: 'webpack/config.eslint.js'
- }
- }
+ config: 'webpack/config.eslint.js',
+ },
+ },
},
rules: {
+ 'import/no-import-module-exports': [
+ 'error',
+ {
+ exceptions: ['**/*/webpack/**/*'],
+ },
+ ],
'arrow-body-style': 'off',
'arrow-parens': 'off',
'class-methods-use-this': 'off',
- 'compat/compat': 'error',
+ 'compat/compat': 'off',
'consistent-return': 'off',
'comma-dangle': 'off',
+ 'default-param-last': 'off',
+ 'func-names': 'off',
'generator-star-spacing': 'off',
+ 'no-nested-ternary': 'off',
'import/no-cycle': 'off',
'import/no-dynamic-require': 'off',
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
@@ -39,23 +48,39 @@ module.exports = {
'jsx-a11y/label-has-associated-control': 'off',
'jsx-a11y/no-static-element-interactions': 'off',
'jsx-a11y/click-events-have-key-events': 'off',
+ 'react/jsx-no-useless-fragment': 'off',
+ 'react/jsx-curly-brace-presence': 'off',
'no-console': [
'error',
{
- allow: ['info', 'error', 'warn']
- }
+ allow: ['info', 'error', 'warn'],
+ },
],
'no-use-before-define': 'off',
+ 'no-restricted-syntax': 'off',
'no-multi-assign': 'off',
+ 'no-promise-executor-return': 'off',
'no-shadow': 'off',
'no-underscore-dangle': 'off',
+ 'no-throw-literal': 'off',
'no-unused-vars': [
'error',
{
args: 'after-used',
- argsIgnorePattern: '^(theme|props|state|ownProps|dispatch|getState)|_',
- varsIgnorePattern: '^(variables|mixins|args|log)'
- }
+ argsIgnorePattern: '^_',
+ varsIgnorePattern: '^_',
+ },
+ ],
+ 'padding-line-between-statements': [
+ 'error',
+ { blankLine: 'always', prev: '*', next: 'return' },
+ { blankLine: 'always', prev: ['block-like'], next: '*' },
+ { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' },
+ {
+ blankLine: 'any',
+ prev: ['const', 'let', 'var'],
+ next: ['const', 'let', 'var'],
+ },
],
'prettier/prettier': ['error', { singleQuote: true }],
'promise/param-names': 'error',
@@ -70,17 +95,16 @@ module.exports = {
'static-methods',
'lifecycle',
'everything-else',
- 'render'
- ]
- }
+ 'render',
+ ],
+ },
],
'react/jsx-filename-extension': ['error', { extensions: ['.js', '.jsx'] }],
'react/jsx-fragments': 'off',
'react/jsx-no-bind': 'off',
'react/jsx-props-no-spreading': 'off',
'react/prefer-stateless-function': 'off',
- strict: 'off',
'react/prop-types': 'off',
- 'react/no-deprecate': 'off'
- }
+ 'react/no-deprecate': 'off',
+ },
};
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 00000000..cfd591ce
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+custom:
+- https://paypal.me/ganeshrvel
+- https://buymeacoffee.com/ganeshrvel
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index fc929e3b..190389d5 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -7,60 +7,18 @@ labels: 'bug'
**Prerequisites**
-If the following boxes are not ALL checked, your issue is likely to be closed.
-
-- [ ] Using yarn
-- [ ] Using node 10.x
-- [ ] Using an up-to-date [`master` branch](https://github.com/ganeshrvel/openmtp/tree/master)
-- [ ] Using latest version of devtools. See [wiki for howto update](https://github.com/electron-react-boilerplate/electron-react-boilerplate/wiki/DevTools)
-- [ ] For issue in production release, devtools output of `DEBUG_PROD=true yarn build && yarn start`
-- [ ] Tried solutions mentioned in [#400](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/400)
+- [ ] Have you reviewed the steps in the following link to see if they address your issue?
+ - https://github.com/ganeshrvel/openmtp/issues/276
**Describe the bug**
A clear and concise description of what the bug is.
-**Expected behaviour**
-If you're describing a bug, tell us what should happen.
-If you're suggesting a change/improvement, tell us how it should work.
-
-**Current Behavior**
- If describing a bug, tell us what happens instead of the expected behaviour.
- If suggesting a change/improvement, explain the difference between current behaviour.
-
- **Possible Solution**
-
- Not obligatory, but suggest a fix/reason for the bug or ideas how to implement the addition or change.
-
-**Steps to Reproduce (for bugs)**
-Provide a link to a live example or an unambiguous set of steps to reproduce this bug. Include code to reproduce, if relevant
-
-1.
-
-2.
-
-3.
-
-4.
-
-**Context**
-How has this issue affected you? What are you trying to accomplish?
-Did you make any changes to the codes after cloning it?
-Providing context helps us come up with a solution that is most useful in the real world.
-
-**Your Environment**
-Include as many relevant details about the environment you experienced the bug in
-
-- Node version :
-- Version or Branch used :
-- Operating System and version [e.g. macOS 10.14 mojave]:
-- App Version [e.g. OpenMTP-v1.0]:
-- Link to your project :
**Attachments**
Include if relevant,
1. Open your Terminal and run the following code:
```shell
-zip -r -X ~/Desktop/OpenMTP-log.zip ~/.io.ganeshrvel/openmtp/logs/
+zip -r -X ~/Desktop/OpenMTP-log.zip "$HOME/Library/Application Support/io.ganeshrvel.openmtp/logs/"
```
2. Attach the file *OpenMTP-log.zip* found in your Desktop folder here.
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 8e9ed081..31bf379b 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -2,18 +2,18 @@ name: CI
on:
push:
- branches: [ master, development ]
+ branches: [ master ]
pull_request:
- branches: [ master, development ]
+ branches: [ master ]
jobs:
build:
- runs-on: macOS-10.15
+ runs-on: macOS-11.00
steps:
- name: Setup build environment
uses: actions/setup-node@v1
with:
- node-version: '10.x'
+ node-version: '16.x'
- run: npm install -g cross-env create-dmg
- name: Cloning Git repository
uses: actions/checkout@v1
@@ -24,7 +24,7 @@ jobs:
- name: Attempt to generate unsigned macOS app
run: |
export CSC_IDENTITY_AUTO_DISCOVERY=false
- yarn package-mac 2>&1 || true
+ yarn package-mac-without-notarize 2>&1 || true
- name: Check if macOS app was generated
run: test -e dist/mac/OpenMTP.app
- name: Attempt to create disk image for generated macOS app
diff --git a/.gitignore b/.gitignore
index 2226b85e..d3551c35 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,10 +52,10 @@ main.js.map
npm-debug.log.*
package-lock.json
.env*
+sentry.properties
app/certs/*.pem
certs/*
-*electron-builder.yml
todo.txt
*yarn-error.log
eslint-common-rules.txt
@@ -63,3 +63,10 @@ eslint-common-rules.txt
build/entitlements.mas.plist
*embedded.provisionprofile
extras/*
+mtp-mock-files/*
+resources
+tmp/*
+*.prod.js
+*.js.map
+*.js.LICENSE.txt
+*.prod.js.LICENSE.txt
diff --git a/.stylelintrc b/.stylelintrc
index 1f2f7f8a..ad16a721 100644
--- a/.stylelintrc
+++ b/.stylelintrc
@@ -1,7 +1,22 @@
{
"extends": ["stylelint-config-standard", "stylelint-config-prettier"],
"rules": {
+ "color-function-notation": null,
+ "value-no-vendor-prefix": null,
+ "property-no-vendor-prefix": null,
+ "font-family-name-quotes": null,
+ "rule-empty-line-before": null,
+ "at-rule-no-vendor-prefix": null,
+ "shorthand-property-no-redundant-values": null,
+ "selector-class-pattern": null,
+ "annotation-no-unknown": null,
+ "value-keyword-case": null,
+ "function-no-unknown": null,
+ "selector-no-vendor-prefix": null,
+ "media-feature-name-no-vendor-prefix": null,
+ "alpha-value-notation": null,
"at-rule-no-unknown": null,
+ "import-notation": null,
"no-descending-specificity": null
}
}
diff --git a/README.md b/README.md
index 04ce4505..0b248254 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,12 @@
# OpenMTP | Android File Transfer for macOS
-- Author: [Ganesh Rathinavel](https://www.linkedin.com/in/ganeshrvel "Ganesh Rathinavel")
-- License: [MIT](https://github.com/ganeshrvel/openmtp/blob/master/LICENSE "MIT")
-- System Requirements: macOS 10.10 or higher
-- Website URL: [https://ganeshrvel.github.io/openmtp](https://ganeshrvel.github.io/openmtp/ "https://ganeshrvel.github.io/openmtp")
-- Repo URL: [https://github.com/ganeshrvel/openmtp](https://github.com/ganeshrvel/openmtp/ "https://github.com/ganeshrvel/openmtp")
+- Author: [Ganesh Rathinavel](https://www.linkedin.com/in/ganeshrvel 'Ganesh Rathinavel')
+- License: [MIT](https://github.com/ganeshrvel/openmtp/blob/master/LICENSE 'MIT')
+- System Requirements: macOS 11.0 (Big Sur) or higher
+- Website URL: [https://openmtp.ganeshrvel.com](https://openmtp.ganeshrvel.com/ 'https://openmtp.ganeshrvel.com')
+- Repo URL: [https://github.com/ganeshrvel/openmtp](https://github.com/ganeshrvel/openmtp/ 'https://github.com/ganeshrvel/openmtp')
- Contacts: ganeshrvel@outlook.com
-
## Introduction
### Advanced Android File Transfer Application for macOS.
@@ -17,65 +16,93 @@ Transferring files between macOS and Android or any other MTP devices has always
Countless searches to find an app to solve these problems and failing to find one made me restless. So, I took the leap and decided to create an app for us that could help us have a smooth and hassle-free file transfer process from macOS to Android/MTP devices. Created with the objective of giving back to the community, we can all use this app for free in this lifetime.
### Features
+
- Safe, Transparent and Open-Source
- Plug and Play via USB. No hassles, easy and instant connection.
- Select between Internal Memory and SD Card
- Transfer multiple files which are larger than 4GB
+- Dark mode
- Drag-and-drop support
- Split pane views for both Local Computer and Android device
- Choose between Grid and List view.
- Use Keyboard Shortcuts to navigate through your files.
- No collection of personally identifiable information.
+### Kalam Kernel
+
+OpenMTP 3.0 features a new MTP kernel and it was written from the scratch. It promises a file copy speed of 30 to 40 MB/s on low and mid range devices and 100 to 120 MB/s on higher end devices. The all new and powerful MTP kernel is named after [Dr. A. P. J. Abdul Kalam](https://en.wikipedia.org/wiki/A._P._J._Abdul_Kalam 'Dr. A. P. J. Abdul Kalam')
+
+Do checkout the Go package which I've written to build Kalam Kernel: [github.com/ganeshrvel/go-mtpx](https://github.com/ganeshrvel/go-mtpx 'https://github.com/ganeshrvel/go-mtpx'). Feel free to raise PRs.
+
+### System Requirements and Support
+
+- To support macOS version below Big Sur the Kalam kernel needs to be compiled on an older macOS machine everytime there is an update, which is practically very difficult
+- Only the latest 3 versions of macOS will receive the `Kalam` Kernel updates, which includes new device supports, fixes, stability improvements. macOS Big Sur (11.0) or above will receive the above said updates
+- We have now officially retired the support for `Kalam` Kernel on macOS 10.13 (OS X El High Sierra) and lower. Only the "Legacy" MTP mode will continue working on these outdated machines.
+- We will continue releasing the updates for both `Intel` and `ARM64` machines
### Installation
-* Download the [Latest Version](https://ganeshrvel.github.io/openmtp/?downloadApp=github&release=stable&platform=mac 'Latest Version')
-* Using Homebrew Cask
- * ```brew cask install openmtp```
+- Download the [Mac Apple Silicon](https://openmtp.ganeshrvel.com/?downloadApp=github&release=stable&platform=mac&arch=arm64 'Mac Apple Silicon') version
+- Download the [Mac Intel Silicon](https://openmtp.ganeshrvel.com/?downloadApp=github&release=stable&platform=mac&arch=x64 'Mac Intel Silicon') version
+- Using Homebrew Cask
-* Find the latest *dmg* file from [GitHub Releases](https://github.com/ganeshrvel/openmtp/releases "GitHub Releases")
+```shell
+ # newer versions:
+ brew install openmtp --cask
+ # older versions:
+ brew cask install openmtp
+```
+
+- Find the latest _dmg_ file from [GitHub Releases](https://github.com/ganeshrvel/openmtp/releases 'GitHub Releases')
### Screengrabs
-
+
-
+
### Keyboard Shortcuts
-| Command | Keyboard Shortcut |
-| ------------------------------------------------- | ------------------------------------------------------------ |
-| Delete | backspace |
-| New Folder | command (⌘)+n |
-| Copy | command (⌘)+c |
-| Copy to Queue | command (⌘)+shift+c |
-| Paste | command (⌘)+v |
-| Refresh | command (⌘) +r |
-| Folder Up | command (⌘)+b |
-| Select All | command (⌘)+a |
-| Rename | command (⌘)+d |
-| Switch Tab | command(⌘)+1 |
-| Open | enter |
-| Navigate Left | left |
-| Navigate Right | right |
-| Navigate Up | up |
-| Navigate Down | down |
-| Select Multiple Items Forward **(in Grid View)** | shift+left |
-| Select Multiple Items Backward **(in Grid View)** | shift+right |
-| Select Multiple Items Forward **(in List View)** | shift+up |
-| Select Multiple Items Backward **(in List View)** | shift+down |
+
+| Command | Keyboard Shortcut |
+| ------------------------------------------------- | ---------------------------------------------------------------------------- |
+| Delete | backspace |
+| New Folder | command (⌘)+n |
+| Copy | command (⌘)+c |
+| Copy to Queue | command (⌘)+shift+c |
+| Paste | command (⌘)+v |
+| Refresh | command (⌘) +r |
+| Folder Up | command (⌘)+b |
+| Select All | command (⌘)+a |
+| Rename | command (⌘)+d |
+| Switch Tab | command(⌘)+1 |
+| Open | enter |
+| Navigate Left | left |
+| Navigate Right | right |
+| Navigate Up | up |
+| Navigate Down | down |
+| Select Multiple Items Forward **(in Grid View)** | shift+left |
+| Select Multiple Items Backward **(in Grid View)** | shift+right |
+| Select Multiple Items Forward **(in List View)** | shift+up |
+| Select Multiple Items Backward **(in List View)** | shift+down |
| Select Multiple Items **(with mouse)** | command (⌘)+click or shift+click |
## Building from Source
-Requirements: [Node.js v10](https://nodejs.org/en/download/ "Install Node.js v10"), [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git "Install Git") and [Yarn package manager](https://yarnpkg.com/lang/en/docs/install/ "Install Yarn package manager")
-
+Requirements: [Node.js v16](https://nodejs.org/en/download/ 'Install Node.js v16'), [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git 'Install Git') and [Yarn package manager](https://yarnpkg.com/lang/en/docs/install/ 'Install Yarn package manager')
### Clone
+
```shell
-$ git clone --depth 1 --single-branch --branch master https://github.com/ganeshrvel/openmtp.git
+$ git clone https://github.com/ganeshrvel/openmtp.git
$ cd openmtp
+
+# install yarn
+npm install -g yarn
+
+# install sentry cli
+npm -g i @sentry/cli
```
```shell
@@ -83,7 +110,8 @@ $ yarn
```
### Run
-A fresh clone might throw *undefined state* error. Run the following commands once to fix the issue.
+
+A fresh clone might throw _undefined state_ error. Run the following commands once to fix the issue.
```shell
# For Mac and Linux
@@ -102,27 +130,117 @@ $ yarn start
```
-### Package
+### Debugging a Packaged app
+
+```shell
+# On terminal run
+$ "/path/to/OpenMTP.app/Contents/MacOS/OpenMTP" --remote-debugging-port=6363
+```
-Setup the *code signing* to build, package and publish the app.
+- Open a Chromium browser
+- Input "about://inspect" into the URL bar
+- Add a new connection `localhost:6363`
+- Inspect OpenMTP @ port `6363`
+
+### Publishing using CI/CD:
+
+- CodeMagic.io
+ - Create a new App (Choose others -> Enter Electron)
+ - Environment variables:
+ - `APPLEID`: ``
+ - `APPLE_APP_SPECIFIC_PASSWORD`: ``
+ - Log into your [Apple Account](https://appleid.apple.com/account/manage 'Apple Account')
+ - Goto **Sign-In and Security > App-Specific Passwords**
+ - Click on **Generate Password...**, enter a password label and click _Create_
+ - Copy the displayed _app-specific-password_
+ - `APPLE_TEAM_ID`: ``
+ - To get Team ID, go to your [Apple Developer Account](https://developer.apple.com/account#MembershipDetailsCard)
+ - Then click on "Membership details", and there you will find your Team ID.
+ - `SENTRY_URL`: `https://sentry.io/`
+ - `SENTRY_ORG`: ``
+ - `SENTRY_PROJECT`: ``
+ - `SENTRY_TOKEN_ID`: ``
+ - Find it from here: [Auth Tokens](https://sentry.io/settings/account/api/auth-tokens)
+ - Scopes: `event:admin, event:read, member:read, org:read, project:read, project:releases, team:read`
+ - `GITHUB_TOKEN`: `Personal access token`
+ - There are two options available:
+ 1. **Fine-grained personal access tokens**
+ - https://github.com/settings/tokens?type=beta
+ - Permissions: `Contents - (Read and Write access to code)`
+ 2. **Personal access tokens (classic)**
+ - https://github.com/settings/tokens
+ - Scopes: `admin:gpg_key, admin:public_key, repo, user, workflow`
+ - `CSC_LINK`:
+ - Keychain -> `Default Keychains` menu in the left -> Login -> My Certificates
+ - Search for `Developer ID Application` in the top search bar
+ - If there are no results for the `Developer ID Application`, for the organization, create one from here: [Apple Developer Certificates](https://developer.apple.com/account/resources/certificates/add)
+ - Follow these steps to get the Apple Developer certificated installed in the local machine [Obtaining-an-Apple-Developer-ID-Certificate-for-macOS-Provisioning](https://forums.ivanti.com/s/article/Obtaining-an-Apple-Developer-ID-Certificate-for-macOS-Provisioning?language=en_US&ui-force-components-controllers-recordGlobalValueProvider.RecordGvp.getRecord=1)
+ - Search for `Developer ID Application` in the top search bar
+ - Expand `Developer ID Application: (XXXYYYZZZ)`
+ - See if the private key's name matches this: `Mac Developer ID Application: `
+ - Else rename the private key as (right click -> get info) `Mac Developer ID Application: `
+ - Close the window
+ - Right Click on the private key -> `Mac Developer ID Application: `
+ - Export `Mac Developer ID Application: `
+ - File name: `CERTIFICATE_PRIVATE_KEY.p12`
+ - Enter Password. This is the `CSC_KEY_PASSWORD`, note this down
+ - Run (this step doesnt work if you are using fig or ohmyzsh, use raw terminal):
+ - `base64 -i CERTIFICATE_PRIVATE_KEY.p12 -o CERTIFICATE_PRIVATE_KEY.txt`
+ - Copy the whole content of the file `CERTIFICATE_PRIVATE_KEY.txt`
+ - Paste the content as the value for the field `CSC_LINK`
+ - `CSC_KEY_PASSWORD` is the password from the above step
+ - `CODEMAGIC_AUTH_TOKEN_ID`: ``
+ - Find it from here: [Sidebar -> Teams -> Personal Account -> Integrations -> Codemagic API](https://codemagic.io/teams)
+ - `CODEMAGIC_INTEL_X64_WORKFLOW_ID_PROD`: ``
+ - Find the relevant workflow id from `codemagic.yaml`, (mostly `macos-intel-x64-build-prod`)
+ - `CODEMAGIC_INTEL_X64_WORKFLOW_ID_DEV`: ``
+ - Find the relevant workflow id from `codemagic.yaml`, (mostly `macos-intel-x64-build-dev`)
+ - `PUBLISH_PROD_REPOSITORY`: ``
+ - `PUBLISH_DEV_REPOSITORY`: ``
+ - `CODEMAGIC_PUBLISH_PROJECT_ID`: ``
+ - `PUBLISH_EMAIL`: `Email address to receive the updates on publish`
+ - References:
+ - [https://www.electron.build/code-signing.html](https://www.electron.build/code-signing.html)
+ - [https://docs.codemagic.io/yaml-code-signing/signing-macos/#saving-the-api-key-to-environment-variables](https://docs.codemagic.io/yaml-code-signing/signing-macos/#saving-the-api-key-to-environment-variables)
+
+### Packaging (locally) and Publishing
+
+Setup the _code signing_ to build, package (locally) and publish the app.
**App Notarization for macOS** (skip this section for non macOS builds)
-- Rename *sample.env* file as *.env*
-- Update APPLEID in *.env* file
-- Log into your [Apple Account](https://appleid.apple.com/account/manage "Apple Account")
-- Goto **Security > App-Specific Passwords**
-- Click on **Generate Password...**, enter a password label and click *Create*
-- Copy the displayed *app-specific-password*
+
+- Rename _sample.env_ file as _.env_
+- To update `APPLEID` and `APPLE_APP_SPECIFIC_PASSWORD` in _.env_ file
+- Log into your [Apple Account](https://appleid.apple.com/account/manage 'Apple Account')
+- Goto **Sign-In and Security > App-Specific Passwords**
+- Click on **Generate Password...**, enter a password label and click _Create_
+- Copy the displayed _app-specific-password_
- Run
+
```shell
-security add-generic-password -a "" -w -s "ELECTRON_NOTORIZE_PASSWORD"
+security add-generic-password -a "" -w -s "APPLE_APP_SPECIFIC_PASSWORD"
```
-- Log into your [Apple App Store Connect Account](https://appstoreconnect.apple.com/agreements/# "Apple App Store Connect Account") and accept the presented terms and conditions
-- The statuses shall turn *Active*
+- Log into your [Apple App Store Connect Account](https://appstoreconnect.apple.com/agreements/# 'Apple App Store Connect Account') and accept the presented terms and conditions
+- The statuses shall turn _Active_
+
+**Sentry**
+
+- Auth Tokens Settings page: [https://sentry.io/settings/account/api/auth-tokens/](https://sentry.io/settings/account/api/auth-tokens/)
+
+```shell
+npm install -g @sentry/wizard
+sentry-wizard --integration electron
+
+# Upload Debug Information
+# Everytime the electron.js version is upgraded run:
+node sentry-symbols.js
+
+sentry-cli login
+```
**Packaging**
-Instructions: [https://www.electron.build/code-signing](https://www.electron.build/code-signing "https://www.electron.build/code-signing")
+Instructions: [https://www.electron.build/code-signing](https://www.electron.build/code-signing 'https://www.electron.build/code-signing')
```shell
$ export GH_TOKEN=""
@@ -136,10 +254,10 @@ $ yarn package
$ yarn package-all
```
-
### Technical Features
-- Built using Electron v6, React v16.8, Redux v4, Webpack v4, Babel v7 and Material UI v4.2 and highly modular
-- Loadables, dynamic reducer injection, selectors for efficient code. splitting and performance/startup optimisation
+
+- Built using Electron v17 and React v18
+- Loadables, Dynamic Reducer Injection, Selectors for code splitting and performance optimization
- Hot module reload (HMR) for instant feedback
- Inbuilt error logging and profile/settings management
- Industry standard state management
@@ -147,23 +265,24 @@ $ yarn package-all
- Port assigned: **4642**
### Configurations
-- *config/env/env.dev.js* and *config/env/env.prod.js* contain the PORT number of the app.
-- *config/dev-app-update.yml* file holds the GitHub repo variables required by *electron-updater*.
-- *config/google-analytics-key.js* file contains the Google Analytics Tracking ID.
-- *package.json* **build.publish** object holds the values for publishing the packaged application.
-- *app/constants* folder contains all the constants required by the app.
+
+- _config/env/env.dev.js_ and _config/env/env.prod.js_ contain the PORT number of the app.
+- _config/dev-app-update.yml_ file holds the GitHub repo variables required by _electron-updater_.
+- _config/google-analytics-key.js_ file contains the Google Analytics Tracking ID.
+- _package.json_ **build.publish** object holds the values for publishing the packaged application.
+- _app/constants_ folder contains all the constants required by the app.
### Debugging
#### **Debugging Guide**
-[https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/400](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/400 "Debugging Guide")
+[https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/400](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/400 'Debugging Guide')
#### **Dispatching redux actions from the main process**
-[https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/118](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/118 "https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/118")
+[https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/118](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/118 'https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/118')
-[https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/108](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/108 "https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/108")
+[https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/108](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/108 'https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/108')
#### **VM112:2 Uncaught TypeError: Cannot read property 'state' of undefined error**
@@ -176,40 +295,75 @@ $ set UPGRADE_EXTENSIONS=1 && npm run dev
```
### Troubleshooting
+
+#### Your device is not recognized
+
+#### **node-mac-permissions** throws `Speech framework is not compatible with macOS < 10.15`
+
+- On macOS <= 10.14.x (mojave) the `yarn install` will throw a npm-rebuild error
+- To "test" or "debug" the app on macOS mojave:
+ - remove the `node-mac-permissions` dependency from `package.json`
+ - Add the ignorePlugin line to `default.plugins` in the file `webpack/config.base.js`
+ - `new webpack.IgnorePlugin({ resourceRegExp: /^(node-mac-permissions)$/u }),`
+ - WARNING: **DO NOT commit** these changes to the upstream!!
+- The `NODE_MAC_PERMISSIONS_MIN_OS` constant defines the minimum os version that is required to show the macos usage access permission popup
+- For distribution make sure to build the app on a machine which is at least 10.15 (Catalina)
+
+[https://stackoverflow.com/questions/58358449/notarizing-electron-apps-throws-you-must-first-sign-the-relevant-contracts-on](https://stackoverflow.com/questions/58358449/notarizing-electron-apps-throws-you-must-first-sign-the-relevant-contracts-on 'https://stackoverflow.com/questions/58358449/notarizing-electron-apps-throws-you-must-first-sign-the-relevant-contracts-on')
+
+- Raise an issue if your device is undetected: https://github.com/ganeshrvel/openmtp/issues/new?template=contribute.md
+
+#### The app goes blank while trying to connect a Samsung device
+
+- Uninstall Samsung SmartSwitch, if installed: [https://farazfazli.medium.com/how-i-reverse-engineered-keis-and-sidesync-and-fixed-mtp-8949acbb1c29](https://farazfazli.medium.com/how-i-reverse-engineered-keis-and-sidesync-and-fixed-mtp-8949acbb1c29 'https://farazfazli.medium.com/how-i-reverse-engineered-keis-and-sidesync-and-fixed-mtp-8949acbb1c29'), [https://github.com/ganeshrvel/openmtp/issues/212](https://github.com/ganeshrvel/openmtp/issues/212 'https://github.com/ganeshrvel/openmtp/issues/212').
+
#### **Notarizing Electron apps throws - “You must first sign the relevant contracts online. (1048)” error**
-[https://stackoverflow.com/questions/58358449/notarizing-electron-apps-throws-you-must-first-sign-the-relevant-contracts-on](https://stackoverflow.com/questions/58358449/notarizing-electron-apps-throws-you-must-first-sign-the-relevant-contracts-on "https://stackoverflow.com/questions/58358449/notarizing-electron-apps-throws-you-must-first-sign-the-relevant-contracts-on")
+[https://stackoverflow.com/questions/58358449/notarizing-electron-apps-throws-you-must-first-sign-the-relevant-contracts-on](https://stackoverflow.com/questions/58358449/notarizing-electron-apps-throws-you-must-first-sign-the-relevant-contracts-on 'https://stackoverflow.com/questions/58358449/notarizing-electron-apps-throws-you-must-first-sign-the-relevant-contracts-on')
### More repos
-- [npm: electron-root-path](https://github.com/ganeshrvel/npm-electron-root-path "Get the root path of an Electron Application")
-- [Electron React Redux Advanced Boilerplate](https://github.com/ganeshrvel/electron-react-redux-advanced-boilerplate "Electron React Redux advanced boilerplate")
-- [Tutorial Series by Ganesh Rathinavel](https://github.com/ganeshrvel/tutorial-series-ganesh-rathinavel "Tutorial Series by Ganesh Rathinavel")
+- [npm: electron-root-path](https://github.com/ganeshrvel/npm-electron-root-path 'Get the root path of an Electron Application')
+- [Electron React Redux Advanced Boilerplate](https://github.com/ganeshrvel/electron-react-redux-advanced-boilerplate 'Electron React Redux advanced boilerplate')
+- [Tutorial Series by Ganesh Rathinavel](https://github.com/ganeshrvel/tutorial-series-ganesh-rathinavel 'Tutorial Series by Ganesh Rathinavel')
### Credits
-- Sincere thanks to [Vladimir Menshakov](https://github.com/whoozle "Vladimir Menshakov") for [android-file-transfer-linux](https://github.com/whoozle/android-file-transfer-linux "android-file-transfer-linux") c++ library and for all his help and support that were extended towards this project.
+- A special thanks to [CodeMagic](http://codemagic.io/ 'Codemagic - CI/CD') and [Kevin Suhajda](https://www.linkedin.com/in/kevinsuhajda 'Kevin Suhajda') for sponsoring their CI/CD VMs, thus making the app releases more streamlined and much easier now. 🎊🎊 Do checkout their [products](https://codemagic.io/integrations 'Codemagic - integrations') section for more.
+
+- Special shoutout to [@CodyJung](https://github.com/CodyJung 'CodyJung') for adding the [Fujifilm](https://github.com/ganeshrvel/go-mtpfs/pull/2) and [Garmin](https://github.com/ganeshrvel/go-mtpfs/pull/1) devices support. 🔥🔥
+
+- Thanks to Ms [Ayushi Bothra](https://www.linkedin.com/in/ayushi-bothra-3103/ 'Ayushi Bothra') for contributing to the documentation and pages.
+
+- App logo was contributed by [Shubhendu Mitra](https://www.linkedin.com/in/shubhendum/ 'Shubhendu Mitra - LinkedIn'). Make sure to check out more of his works on [Behance](https://www.behance.net/soponhara 'Shubhendu Mitra - Behance').
-- Thanks to Ms [Ayushi Bothra](https://www.linkedin.com/in/ayushi-bothra-3103/ "Ayushi Bothra") for contributing to the documentation and pages.
+- Thanks to [Vladimir Menshakov](https://github.com/whoozle 'Vladimir Menshakov') for [android-file-transfer-linux](https://github.com/whoozle/android-file-transfer-linux 'android-file-transfer-linux') (the MTP legacy Kernel)
-- This app is built upon [https://github.com/ganeshrvel/electron-react-redux-advanced-boilerplate](https://github.com/ganeshrvel/electron-react-redux-advanced-boilerplate "https://github.com/ganeshrvel/electron-react-redux-advanced-boilerplate") which is a heavily modified fork of [https://github.com/electron-react-boilerplate/electron-react-boilerplate](https://github.com/electron-react-boilerplate/electron-react-boilerplate "https://github.com/electron-react-boilerplate/electron-react-boilerplate").
+- Shoutout to [@yennsarah](https://github.com/yennsarah 'yennsarah'), [@h0tk3y](https://github.com/h0tk3y), [@riginoommen](https://github.com/riginoommen 'riginoommen'), [@AjithKumarvm](https://github.com/AjithKumarvm 'AjithKumarvm'), [@kiranshaji555](https://github.com/kiranshaji555), Dick Cowan, Kjell Dankert, Thorolf E.R. Weißhuhn and to all other community members who helped me test the application.
-- The icons used in the app were made by [Prosymbols](https://www.flaticon.com/authors/prosymbols "Prosymbols"), [Nikita Golubev](https://www.flaticon.com/authors/nikita-golubev "Nikita Golubev"), [Those Icons](https://www.flaticon.com/authors/those-icons "Those Icons"), [Kiranshastry](https://www.flaticon.com/authors/kiranshastry "Kiranshastry"), [Elias Bikbulatov](https://www.flaticon.com/authors/elias-bikbulatov "Elias Bikbulatov") & [Pixel perfect](https://www.flaticon.com/authors/pixel-perfect "Pixel perfect") and were distributed by [www.flaticon.com](https://www.flaticon.com/ "Flaticon") which is licensed under [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/ "Creative Commons BY 3.0").
+- This app was built upon [https://github.com/ganeshrvel/electron-react-redux-advanced-boilerplate](https://github.com/ganeshrvel/electron-react-redux-advanced-boilerplate 'https://github.com/ganeshrvel/electron-react-redux-advanced-boilerplate') which is a heavily modified fork of [https://github.com/electron-react-boilerplate/electron-react-boilerplate](https://github.com/electron-react-boilerplate/electron-react-boilerplate 'https://github.com/electron-react-boilerplate/electron-react-boilerplate').
-- The "no image found" icon was made by [Phonlaphat Thongsriphong](https://www.iconfinder.com/phatpc "Phonlaphat Thongsriphong").
+- The icons used in the app were made by [flaticon](https://www.flaticon.com), [good-ware](https://www.flaticon.com/authors/good-ware) and [kiranshastry](https://www.flaticon.com/authors/kiranshastry) which is licensed under [CC 3.0 BY](https://creativecommons.org/licenses/by/3.0/ 'Creative Commons BY 3.0').
+- The "no image found" icon was made by [Phonlaphat Thongsriphong](https://www.iconfinder.com/phatpc 'Phonlaphat Thongsriphong').
### Contribute
-If you are interested in fixing issues and contributing directly to the code base, please see the [guidelines](https://github.com/ganeshrvel/openmtp/blob/master/CONTRIBUTING.md "guidelines").
+
+If you are interested in fixing issues and contributing directly to the code base, please see the [guidelines](https://github.com/ganeshrvel/openmtp/blob/master/CONTRIBUTING.md 'guidelines').
### Support OpenMTP
+
Help me keep the app FREE and open for all.
-Paypal: [paypal.me/ganeshrvel](https://paypal.me/ganeshrvel "paypal.me/ganeshrvel")
+
+- Support us via PayPal: [paypal.me/ganeshrvel](https://paypal.me/ganeshrvel 'https://paypal.me/ganeshrvel')
+- Buy Me A Coffee (UPI, PayPal, Credit/Debit Cards, Internet Banking): [buymeacoffee.com/ganeshrvel](https://buymeacoffee.com/ganeshrvel 'https://buymeacoffee.com/ganeshrvel')
### Contacts
+
Please feel free to contact me at ganeshrvel@outlook.com
### License
-OpenMTP | Android File Transfer for macOS is released under [MIT License](https://github.com/ganeshrvel/openmtp/blob/master/LICENSE "MIT License").
+
+OpenMTP | Android File Transfer for macOS is released under [MIT License](https://github.com/ganeshrvel/openmtp/blob/master/LICENSE 'MIT License').
Copyright © 2018-Present Ganesh Rathinavel
diff --git a/app/api/sys/index.js b/app/api/sys/index.js
deleted file mode 100644
index 7d5f2531..00000000
--- a/app/api/sys/index.js
+++ /dev/null
@@ -1,974 +0,0 @@
-'use strict';
-
-/* eslint no-await-in-loop: off */
-
-import {
- readdir as fsReaddir,
- rename as fsRename,
- existsSync,
- statSync,
- lstatSync
-} from 'fs';
-import Promise from 'bluebird';
-import junk from 'junk';
-import rimraf from 'rimraf';
-import mkdirp from 'mkdirp';
-import path from 'path';
-import moment from 'moment';
-import { spawn, exec } from 'child_process';
-import findLodash from 'lodash/find';
-import { log } from '../../utils/log';
-import { mtp as _mtpCli } from '../../utils/binaries';
-
-import { DEVICES_LABEL, DEVICES_TYPE_CONST } from '../../constants';
-import { baseName, getExtension } from '../../utils/paths';
-import {
- clearFileTransfer,
- fetchDirList,
- processMtpOutput,
- setFileTransferProgress
-} from '../../containers/HomePage/actions';
-import {
- niceBytes,
- percentage,
- splitIntoLines,
- truncate,
- isArray,
- undefinedOrNull
-} from '../../utils/funcs';
-import { msToTime, unixTimestampNow } from '../../utils/date';
-
-const readdir = Promise.promisify(fsReaddir);
-const execPromise = Promise.promisify(exec);
-
-/**
- * This hack is to support flex quotes parser for mtp cli.
- */
-export const escapeShellMtp = cmd => {
- if (cmd.indexOf(`\\"`) !== -1 && cmd.indexOf(`"\\`) !== -1) {
- return cmd
- .replace(/`/g, '\\`')
- .replace(/\\/g, `\\\\\\\\`)
- .replace(/"/g, `\\\\\\"`);
- }
- if (cmd.indexOf(`"\\"`) !== -1) {
- return cmd
- .replace(/`/g, '\\`')
- .replace(/\\/g, `\\\\\\\\`)
- .replace(/"/g, `\\\\\\"`);
- }
- if (cmd.indexOf(`\\"`) !== -1) {
- return cmd
- .replace(/`/g, '\\`')
- .replace(/\\/g, `\\\\\\`)
- .replace(/"/g, `\\\\\\\\"`);
- }
- if (cmd.indexOf(`"\\`) !== -1) {
- return cmd
- .replace(/`/g, '\\`')
- .replace(/\\/g, `\\\\\\\\`)
- .replace(/"/g, `\\\\\\"`);
- }
- return cmd
- .replace(/`/g, '\\`')
- .replace(/\\/g, `\\\\\\`)
- .replace(/"/g, `\\\\\\"`);
-};
-
-const mtpCli = `"${escapeShellMtp(_mtpCli)}"`;
-
-const filterOutMtpLines = (string, index) => {
- return (
- filterJunkMtpErrors(string) ||
- (index < 2 && string.toLowerCase().indexOf(`selected storage`) !== -1)
- );
-};
-
-const filterJunkMtpErrors = string => {
- return (
- string === '\n' ||
- string === '\r\n' ||
- string === '' ||
- string.toLowerCase().indexOf(`device::find failed`) !== -1 ||
- string.toLowerCase().indexOf(`iocreateplugininterfaceforservice`) !== -1 ||
- string.toLowerCase().indexOf(`Device::Find failed`) !== -1
- );
-};
-
-const cleanJunkMtpError = ({ error = null, stdout = null, stderr = null }) => {
- const splittedError = splitIntoLines(error);
- const filteredError = splittedError
- ? splittedError.filter(a => !filterJunkMtpErrors(a))
- : [];
-
- const splittedStderr = splitIntoLines(stderr);
- const filteredStderr = splittedStderr
- ? splittedStderr.filter(a => !filterJunkMtpErrors(a))
- : [];
-
- return {
- filteredError,
- filteredStderr,
- filteredStdout: stdout
- };
-};
-
-const promisifiedExec = command => {
- try {
- return new Promise(resolve => {
- execPromise(command, (error, stdout, stderr) => {
- const {
- filteredStderr,
- filteredError,
- filteredStdout
- } = cleanJunkMtpError({ error, stdout, stderr });
-
- if (
- (undefinedOrNull(filteredStderr) || filteredStderr.length < 1) &&
- (undefinedOrNull(filteredError) || filteredError.length < 1)
- ) {
- return resolve({
- data: filteredStdout,
- stderr: null,
- error: null
- });
- }
-
- return resolve({
- data: filteredStdout,
- stderr: filteredStderr.join('\n'),
- error: filteredError.join('\n')
- });
- });
- });
- } catch (e) {
- log.error(e);
- }
-};
-
-const promisifiedExecNoCatch = command => {
- return new Promise(resolve => {
- execPromise(command, (error, stdout, stderr) => {
- const {
- filteredStderr,
- filteredError,
- filteredStdout
- } = cleanJunkMtpError({ error, stdout, stderr });
-
- if (
- (undefinedOrNull(filteredStderr) || filteredStderr.length < 1) &&
- (undefinedOrNull(filteredError) || filteredError.length < 1)
- ) {
- return resolve({
- data: filteredStdout,
- stderr: null,
- error: null
- });
- }
-
- return resolve({
- data: stdout,
- stderr,
- error
- });
- });
- });
-};
-
-const checkMtpFileExists = async (filePath, mtpStoragesListSelected) => {
- const storageSelectCmd = `"storage ${mtpStoragesListSelected}"`;
- const escapedFilePath = `${escapeShellMtp(filePath)}`;
-
- const { stderr } = await promisifiedExecNoCatch(
- `${mtpCli} ${storageSelectCmd} "properties \\"${escapedFilePath}\\""`
- );
-
- return !stderr;
-};
-
-export const checkFileExists = async (
- filePath,
- deviceType,
- mtpStoragesListSelected
-) => {
- try {
- if (typeof filePath === 'undefined' || filePath === null) {
- return null;
- }
-
- let _isArray = false;
- if (isArray(filePath)) {
- _isArray = true;
- }
-
- let fullPath = null;
- switch (deviceType) {
- case DEVICES_TYPE_CONST.local:
- if (_isArray) {
- for (let i = 0; i < filePath.length; i += 1) {
- const item = filePath[i];
- fullPath = path.resolve(item);
- if (await existsSync(fullPath)) {
- return true;
- }
- }
- return null;
- }
-
- fullPath = path.resolve(filePath);
- return await existsSync(fullPath);
-
- case DEVICES_TYPE_CONST.mtp:
- if (_isArray) {
- for (let i = 0; i < filePath.length; i += 1) {
- const item = filePath[i];
- fullPath = path.resolve(item);
- if (await checkMtpFileExists(fullPath, mtpStoragesListSelected)) {
- return true;
- }
- }
- return null;
- }
-
- fullPath = path.resolve(filePath);
- return await checkMtpFileExists(fullPath, mtpStoragesListSelected);
-
- default:
- break;
- }
-
- return true;
- } catch (e) {
- log.error(e);
- }
-};
-
-/**
- Local device ->
- */
-export const asyncReadLocalDir = async ({ filePath, ignoreHidden }) => {
- try {
- const response = [];
- const { error, data } = await readdir(filePath, 'utf8')
- .then(res => {
- return {
- data: res,
- error: null
- };
- })
- .catch(e => {
- return {
- data: null,
- error: e
- };
- });
-
- if (error) {
- log.error(error, `asyncReadLocalDir`);
- return { error: true, data: null };
- }
-
- let files = data;
-
- files = data.filter(junk.not);
- if (ignoreHidden) {
- // eslint-disable-next-line no-useless-escape
- files = data.filter(item => !/(^|\/)\.[^\/\.]/g.test(item));
- }
-
- for (let i = 0; i < files.length; i += 1) {
- const file = files[i];
- const fullPath = path.resolve(filePath, file);
-
- if (!existsSync(fullPath)) {
- continue; // eslint-disable-line no-continue
- }
- const stat = statSync(fullPath);
- const isFolder = lstatSync(fullPath).isDirectory();
- const extension = path.extname(fullPath);
- const { size, atime: dateTime } = stat;
-
- if (findLodash(response, { path: fullPath })) {
- continue; // eslint-disable-line no-continue
- }
-
- response.push({
- name: file,
- path: fullPath,
- extension,
- size,
- isFolder,
- dateAdded: moment(dateTime).format('YYYY-MM-DD HH:mm:ss')
- });
- }
- return { error, data: response };
- } catch (e) {
- log.error(e);
- }
-};
-
-export const promisifiedRimraf = item => {
- try {
- return new Promise(resolve => {
- rimraf(item, {}, error => {
- resolve({
- data: null,
- stderr: error,
- error
- });
- });
- });
- } catch (e) {
- log.error(e);
- }
-};
-
-export const delLocalFiles = async ({ fileList }) => {
- try {
- if (!fileList || fileList.length < 1) {
- return { error: `No files selected.`, stderr: null, data: null };
- }
-
- for (let i = 0; i < fileList.length; i += 1) {
- const item = fileList[i];
- const { error } = await promisifiedRimraf(item);
- if (error) {
- log.error(`${error}`, `delLocalFiles -> rm error`);
- return { error, stderr: null, data: false };
- }
- }
-
- return { error: null, stderr: null, data: true };
- } catch (e) {
- log.error(e);
- }
-};
-
-const promisifiedRename = ({ oldFilePath, newFilePath }) => {
- try {
- return new Promise(resolve => {
- fsRename(oldFilePath, newFilePath, error => {
- resolve({
- data: null,
- stderr: error,
- error
- });
- });
- });
- } catch (e) {
- log.error(e);
- }
-};
-
-export const renameLocalFiles = async ({ oldFilePath, newFilePath }) => {
- try {
- if (
- typeof oldFilePath === 'undefined' ||
- oldFilePath === null ||
- typeof newFilePath === 'undefined' ||
- newFilePath === null
- ) {
- return { error: `No files selected.`, stderr: null, data: null };
- }
-
- const { error } = await promisifiedRename({ oldFilePath, newFilePath });
- if (error) {
- log.error(`${error}`, `renameLocalFiles -> mv error`);
- return { error, stderr: null, data: false };
- }
-
- return { error: null, stderr: null, data: true };
- } catch (e) {
- log.error(e);
- }
-};
-
-const promisifiedMkdir = ({ newFolderPath }) => {
- try {
- return new Promise(resolve => {
- mkdirp(newFolderPath, error => {
- resolve({ data: null, stderr: error, error });
- });
- });
- } catch (e) {
- log.error(e);
- }
-};
-
-export const newLocalFolder = async ({ newFolderPath }) => {
- try {
- if (typeof newFolderPath === 'undefined' || newFolderPath === null) {
- return { error: `Invalid path.`, stderr: null, data: null };
- }
-
- const { error } = await promisifiedMkdir({ newFolderPath });
- if (error) {
- log.error(`${error}`, `newLocalFolder -> mkdir error`);
- return { error, stderr: null, data: false };
- }
-
- return { error: null, stderr: null, data: true };
- } catch (e) {
- log.error(e);
- }
-};
-
-/**
- MTP device ->
- */
-export const fetchMtpStorageOptions = async () => {
- try {
- const { data, error, stderr } = await promisifiedExec(
- `${mtpCli} "storage-list"`
- );
-
- if (error || stderr) {
- log.error(
- `${error} : ${stderr}`,
- `fetchMtpStorageOptions -> storage-list error`,
- false
- );
- return { error, stderr, data: null };
- }
-
- const _storageList = splitIntoLines(data);
-
- const descMatchPattern = /description:(.*)/i;
- const storageIdMatchPattern = /([^\D]+)/;
-
- let storageList = {};
- _storageList
- .filter((a, index) => !filterOutMtpLines(a, index))
- .map((a, index) => {
- if (!a) {
- return null;
- }
- const _matchDesc = descMatchPattern.exec(a);
- const _matchedStorageId = storageIdMatchPattern.exec(a);
-
- if (
- typeof _matchDesc === 'undefined' ||
- _matchDesc === null ||
- typeof _matchDesc[1] === 'undefined' ||
- typeof _matchedStorageId === 'undefined' ||
- _matchedStorageId === null ||
- typeof _matchedStorageId[1] === 'undefined'
- ) {
- return null;
- }
-
- const matchDesc = _matchDesc[1].trim();
- const matchedStorageId = _matchedStorageId[1].trim();
- storageList = {
- ...storageList,
- [matchedStorageId]: {
- name: matchDesc,
- selected: index === 0
- }
- };
-
- return storageList;
- });
-
- if (
- typeof storageList === 'undefined' ||
- storageList === null ||
- storageList.length < 1
- ) {
- return {
- error: `${
- DEVICES_LABEL[DEVICES_TYPE_CONST.mtp]
- } storage not accessible`,
- stderr: null,
- data: null
- };
- }
-
- return { error: null, stderr: null, data: storageList };
- } catch (e) {
- log.error(e);
- }
-};
-
-export const asyncReadMtpDir = async ({
- filePath,
- ignoreHidden,
- mtpStoragesListSelected
-}) => {
- try {
- const mtpCmdChopIndex = {
- type: 2,
- dateAdded: 4,
- timeAdded: 5
- };
- const response = [];
- const storageSelectCmd = `"storage ${mtpStoragesListSelected}"`;
-
- const {
- data: filePropsData,
- error: filePropsError,
- stderr: filePropsStderr
- } = await promisifiedExec(
- `${mtpCli} ${storageSelectCmd} "lsext \\"${escapeShellMtp(filePath)}\\""`
- );
-
- if (filePropsError || filePropsStderr) {
- log.error(
- `${filePropsError} : ${filePropsStderr}`,
- `asyncReadMtpDir -> lsext error`
- );
- return { error: filePropsError, stderr: filePropsStderr, data: null };
- }
-
- let fileProps = splitIntoLines(filePropsData);
-
- fileProps = fileProps.filter((a, index) => !filterOutMtpLines(a, index));
-
- for (let i = 0; i < fileProps.length; i += 1) {
- const item = fileProps[i];
- const matchedProps = item.match(
- /^(.*?)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}/g
- );
-
- if (matchedProps === null || matchedProps.length < 1) {
- continue; // eslint-disable-line no-continue
- }
-
- const _matchedProps = matchedProps[0];
- const itemSplit = item.split(_matchedProps);
-
- if (itemSplit === null || itemSplit.length < 2 || itemSplit[1] === '') {
- continue; // eslint-disable-line no-continue
- }
- const matchedFileName = itemSplit[1].replace(/^\s{2}|\s$/g, '');
- const filePropsList = _matchedProps.replace(/\s\s+/g, ' ').split(' ');
-
- // eslint-disable-next-line no-useless-escape
- if (ignoreHidden && /(^|\/)\.[^\/\.]/g.test(matchedFileName)) {
- continue; // eslint-disable-line no-continue
- }
-
- const fullPath = path.resolve(filePath, matchedFileName);
- const isFolder = filePropsList[mtpCmdChopIndex.type] === '3001';
- const dateTime = `${filePropsList[mtpCmdChopIndex.dateAdded]} ${
- filePropsList[mtpCmdChopIndex.timeAdded]
- }`;
- const extension = getExtension(fullPath, isFolder);
-
- // avoid duplicate values
- if (findLodash(response, { path: fullPath })) {
- continue; // eslint-disable-line no-continue
- }
- response.push({
- name: matchedFileName,
- path: fullPath,
- extension,
- size: null,
- isFolder,
- dateAdded: moment(dateTime).format('YYYY-MM-DD HH:mm:ss')
- });
- }
-
- return { error: null, stderr: null, data: response };
- } catch (e) {
- log.error(e);
- }
-};
-
-export const renameMtpFiles = async ({
- oldFilePath,
- newFilePath,
- mtpStoragesListSelected
-}) => {
- try {
- if (
- typeof oldFilePath === 'undefined' ||
- oldFilePath === null ||
- typeof newFilePath === 'undefined' ||
- newFilePath === null
- ) {
- return { error: `No files selected.`, stderr: null, data: null };
- }
-
- const storageSelectCmd = `"storage ${mtpStoragesListSelected}"`;
- const escapedOldFilePath = `${escapeShellMtp(oldFilePath)}`;
- const escapedNewFilePath = `${escapeShellMtp(baseName(newFilePath))}`;
-
- const { error, stderr } = await promisifiedExec(
- `${mtpCli} ${storageSelectCmd} "rename \\"${escapedOldFilePath}\\" \\"${escapedNewFilePath}\\""`
- );
-
- if (error || stderr) {
- log.error(`${error} : ${stderr}`, `renameMtpFiles -> rename error`);
- return { error, stderr, data: false };
- }
-
- return { error: null, stderr: null, data: true };
- } catch (e) {
- log.error(e);
- }
-};
-
-export const delMtpFiles = async ({ fileList, mtpStoragesListSelected }) => {
- try {
- if (!fileList || fileList.length < 1) {
- return { error: `No files selected.`, stderr: null, data: null };
- }
-
- const storageSelectCmd = `"storage ${mtpStoragesListSelected}"`;
- for (let i = 0; i < fileList.length; i += 1) {
- const { error, stderr } = await promisifiedExec(
- `${mtpCli} ${storageSelectCmd} "rm \\"${escapeShellMtp(
- fileList[i]
- )}\\""`
- );
-
- if (error || stderr) {
- log.error(`${error} : ${stderr}`, `delMtpDir -> rm error`);
- return { error, stderr, data: false };
- }
- }
-
- return { error: null, stderr: null, data: true };
- } catch (e) {
- log.error(e);
- }
-};
-
-export const newMtpFolder = async ({
- newFolderPath,
- mtpStoragesListSelected
-}) => {
- try {
- if (typeof newFolderPath === 'undefined' || newFolderPath === null) {
- return { error: `Invalid path.`, stderr: null, data: null };
- }
-
- const storageSelectCmd = `"storage ${mtpStoragesListSelected}"`;
- const escapedNewFolderPath = `${escapeShellMtp(newFolderPath)}`;
- const { error, stderr } = await promisifiedExec(
- `${mtpCli} ${storageSelectCmd} "mkpath \\"${escapedNewFolderPath}\\""`
- );
-
- if (error || stderr) {
- log.error(`${error} : ${stderr}`, `newMtpFolder -> mkpath error`);
- return { error, stderr, data: false };
- }
-
- return { error: null, stderr: null, data: true };
- } catch (e) {
- log.error(e);
- }
-};
-
-export const pasteFiles = (
- { ...pasteArgs },
- { ...fetchDirListArgs },
- direction,
- deviceType,
- dispatch,
- getState,
- getCurrentWindow
-) => {
- try {
- const {
- destinationFolder,
- mtpStoragesListSelected,
- fileTransferClipboard
- } = pasteArgs;
-
- if (
- typeof destinationFolder === 'undefined' ||
- destinationFolder === null
- ) {
- dispatch(
- processMtpOutput({
- deviceType,
- error: `Invalid path.`,
- stderr: null,
- data: null,
- callback: () => {
- dispatch(
- fetchDirList({ ...fetchDirListArgs }, deviceType, getState)
- );
- }
- })
- );
- }
-
- const storageSelectCmd = `"storage ${mtpStoragesListSelected}"`;
- const { queue } = fileTransferClipboard;
-
- if (typeof queue === 'undefined' || queue === null || queue.length < 1) {
- dispatch(
- processMtpOutput({
- deviceType,
- error: `No files selected`,
- stderr: null,
- data: null,
- callback: () => {
- dispatch(
- fetchDirList({ ...fetchDirListArgs }, deviceType, getState)
- );
- }
- })
- );
- }
-
- let _queue = [];
- let cmdArgs = {};
- switch (direction) {
- case 'mtpToLocal':
- _queue = (queue || []).map(sourcePath => {
- const destinationPath = path.resolve(destinationFolder);
- const escapedDestinationPath = escapeShellMtp(
- `${destinationPath}/${baseName(sourcePath)}`
- );
- const escapedSourcePath = `${escapeShellMtp(sourcePath)}`;
-
- return `-e ${storageSelectCmd} "get \\"${escapedSourcePath}\\" \\"${escapedDestinationPath}\\""`;
- });
-
- cmdArgs = {
- _queue
- };
- return _pasteFiles(
- { ...pasteArgs },
- { ...fetchDirListArgs },
- { ...cmdArgs },
- deviceType,
- dispatch,
- getState,
- getCurrentWindow
- );
-
- case 'localtoMtp':
- _queue = (queue || []).map(sourcePath => {
- const destinationPath = path.resolve(destinationFolder);
- const escapedDestinationPath = `${escapeShellMtp(destinationPath)}`;
- const escapedSourcePath = `${escapeShellMtp(sourcePath)}`;
-
- return `-e ${storageSelectCmd} "put \\"${escapedSourcePath}\\" \\"${escapedDestinationPath}\\""`;
- });
-
- cmdArgs = {
- _queue
- };
-
- return _pasteFiles(
- { ...pasteArgs },
- { ...fetchDirListArgs },
- { ...cmdArgs },
- deviceType,
- dispatch,
- getState,
- getCurrentWindow
- );
-
- default:
- break;
- }
- } catch (e) {
- log.error(e);
- }
-};
-
-const _pasteFiles = (
- { ...pasteArgs }, // eslint-disable-line no-unused-vars
- { ...fetchDirListArgs }, // eslint-disable-line no-unused-vars
- { ...cmdArgs },
- deviceType,
- dispatch,
- getState,
- getCurrentWindow
-) => {
- try {
- const { _queue } = cmdArgs;
- const handletransferListTimeInterval = 1000;
- let transferList = {};
- let prevCopiedBlockSize = 0;
- let currentCopiedBlockSize = 0;
- let startTime = 0;
- let prevCopiedTime = 0;
- let currentCopiedTime = 0;
- let bufferedOutput = null;
-
- let handleTransferListInterval = setInterval(() => {
- if (transferList === null) {
- clearInterval(handleTransferListInterval);
- handleTransferListInterval = 0;
- return null;
- }
-
- if (Object.keys(transferList).length < 1) {
- return null;
- }
-
- const { percentage: _percentage, bodyText1, bodyText2 } = transferList;
- const copiedTimeDiff = currentCopiedTime - prevCopiedTime;
- const speed =
- prevCopiedBlockSize && prevCopiedBlockSize - currentCopiedBlockSize > 0
- ? (prevCopiedBlockSize - currentCopiedBlockSize) *
- (1000 / copiedTimeDiff)
- : 0;
- const _speed = speed ? `${niceBytes(speed)}` : `--`;
- const elapsedTime = msToTime(currentCopiedTime - startTime);
- prevCopiedTime = currentCopiedTime;
- prevCopiedBlockSize = currentCopiedBlockSize;
-
- getCurrentWindow().setProgressBar(_percentage / 100);
- dispatch(
- setFileTransferProgress({
- toggle: true,
- bodyText1,
- bodyText2: `Elapsed: ${elapsedTime} | Progress: ${bodyText2} @ ${_speed}/sec`,
- percentage: _percentage
- })
- );
- }, handletransferListTimeInterval);
-
- const cmd = spawn(mtpCli, [..._queue], {
- shell: true
- });
-
- cmd.stdout.on('data', data => {
- bufferedOutput = data.toString();
-
- if (startTime === 0) {
- startTime = unixTimestampNow();
- }
-
- if (
- typeof bufferedOutput === 'undefined' ||
- bufferedOutput === null ||
- bufferedOutput.length < 1
- ) {
- return null;
- }
-
- const _bufferedOutput = splitIntoLines(bufferedOutput).filter(
- (a, index) => !filterOutMtpLines(a, index)
- );
-
- if (_bufferedOutput.length < 1) {
- return null;
- }
-
- for (let i = 0; i < _bufferedOutput.length; i += 1) {
- const item = _bufferedOutput[i];
- const bufferedOutputSplit = item.split(' ');
-
- if (bufferedOutputSplit.length < 1) {
- return null;
- }
-
- const totalLength = bufferedOutputSplit.length;
- const eventIndex = 0;
- const filePathStartIndex = 1;
- const filePathEndIndex = totalLength - 3;
- const currentProgressSizeIndex = totalLength - 2;
- const totalFileSizeIndex = totalLength - 1;
-
- const event = bufferedOutputSplit[eventIndex];
- const matchedItem = item.match(/(\d+?\d*)\s(\d+?\d*)$/);
- if (matchedItem === null) {
- return null;
- }
-
- const matchedItemSplit = matchedItem[0].split(' ');
- const currentProgressSize = parseInt(matchedItemSplit[0], 10);
- const totalFileSize = parseInt(matchedItemSplit[1], 10);
-
- if (event === `:done`) {
- prevCopiedBlockSize = 0;
- currentCopiedBlockSize = 0;
- prevCopiedTime = 0;
- currentCopiedTime = 0;
- return null;
- }
-
- if (
- totalLength < 3 ||
- event !== `:progress` ||
- currentProgressSizeIndex < 2 ||
- totalFileSizeIndex < 3
- ) {
- return null;
- }
-
- const filePath = bufferedOutputSplit
- .slice(filePathStartIndex, filePathEndIndex + 1)
- .join(' ');
-
- const perc = percentage(currentProgressSize, totalFileSize);
- currentCopiedBlockSize = totalFileSize - currentProgressSize;
- currentCopiedTime = unixTimestampNow();
-
- transferList = {
- bodyText1: `${perc}% complete of ${truncate(baseName(filePath), 45)}`,
- bodyText2: `${niceBytes(currentProgressSize)} / ${niceBytes(
- totalFileSize
- )}`,
- percentage: perc,
- currentCopiedBlockSize,
- currentCopiedTime
- };
- }
- });
-
- cmd.stderr.on('data', error => {
- const { filteredError } = cleanJunkMtpError({ error });
-
- if (undefinedOrNull(filteredError) || filteredError.length < 1) {
- return null;
- }
-
- dispatch(
- processMtpOutput({
- deviceType,
- error,
- stderr: null,
- data: null,
- callback: () => {
- transferList = null;
- getCurrentWindow().setProgressBar(-1);
- dispatch(clearFileTransfer());
- dispatch(
- fetchDirList({ ...fetchDirListArgs }, deviceType, getState)
- );
- }
- })
- );
- });
-
- cmd.on('exit', () => {
- transferList = null;
- getCurrentWindow().setProgressBar(-1);
- dispatch(clearFileTransfer());
- dispatch(fetchDirList({ ...fetchDirListArgs }, deviceType, getState));
- });
-
- return { error: null, stderr: null, data: true };
- } catch (e) {
- log.error(e);
- }
-};
-
-export const mtpVerboseReport = async () => {
- try {
- const { data, error, stderr } = await promisifiedExec(`${mtpCli} "pwd" -v`);
-
- if (error) {
- log.doLog(`${error}`);
- return { error, stderr, data: null };
- }
- if (stderr) {
- log.doLog(`${stderr}`);
- return { error, stderr, data: null };
- }
-
- return { error: null, stderr: null, data };
- } catch (e) {
- log.error(e);
- }
-};
diff --git a/app/api/www/index.js b/app/api/www/index.js
deleted file mode 100644
index 1bfe2890..00000000
--- a/app/api/www/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-'use strict';
-
-export function fetchUrl({ url }) {
- return fetch(`${url}`).then(res => {
- if (res.status === 200) {
- return res.json();
- }
- return null;
- });
-}
diff --git a/app/app.html b/app/app.html
index b8c39c21..a5ac4b08 100644
--- a/app/app.html
+++ b/app/app.html
@@ -1,45 +1,57 @@
-
-
-
- OpenMTP | Android File Transfer for macOS
-
-
-
-
-
+
+
+
+
-
+ PORT = require("../config/env").PORT;
+ }
+ if (process.env.START_HOT) {
+ // Dynamically insert the bundled app script in the renderer process
+ scripts.push(`http://localhost:${PORT}/dist/renderer.dev.js`);
+ } else {
+ scripts.push("./dist/renderer.prod.js");
+ }
+
+ if (scripts.length) {
+ document.write(
+ scripts
+ .map(script => `
+