├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── app ├── app.dev.html ├── app.global.css ├── app.icns ├── app.prod.html ├── components │ ├── FileItem │ │ ├── index.css │ │ └── index.tsx │ ├── ReceiveFileItem │ │ ├── index.css │ │ └── index.tsx │ └── SendFileItem │ │ ├── index.css │ │ └── index.tsx ├── containers │ ├── App │ │ ├── index.css │ │ └── index.tsx │ └── Home │ │ ├── index.css │ │ └── index.tsx ├── index.tsx ├── main.prod.js.LICENSE.txt ├── main.ts ├── native.ts ├── package.json ├── types │ └── index.ts └── utils │ └── index.ts ├── babel.config.js ├── configs ├── .eslintrc ├── webpack.config.base.js ├── webpack.config.eslint.js ├── webpack.config.main.prod.babel.js ├── webpack.config.renderer.dev.babel.js ├── webpack.config.renderer.dev.dll.babel.js └── webpack.config.renderer.prod.babel.js ├── demo.png ├── internals └── scripts │ ├── .eslintrc │ ├── babel-register.js │ ├── check-builds-exist.js │ ├── check-native-dep.js │ ├── check-node-env.js │ ├── check-port-in-use.js │ ├── check-yarn.js │ ├── delete-source-maps.js │ └── electron-rebuild.js ├── native ├── .gitignore ├── Cargo.toml ├── build.rs ├── rustfmt.toml └── src │ ├── error.rs │ ├── ext │ ├── eh.rs │ ├── json_stream.rs │ ├── mod.rs │ └── object.rs │ ├── helpers │ └── mod.rs │ ├── lib.rs │ ├── prelude │ └── mod.rs │ ├── runtime │ └── mod.rs │ ├── transfer │ ├── client │ │ └── mod.rs │ ├── mod.rs │ └── server │ │ └── mod.rs │ ├── types │ └── mod.rs │ └── utils │ └── mod.rs ├── package.json ├── resources ├── icon.icns ├── icon.ico ├── icon.png └── icons │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ ├── 64x64.png │ └── 96x96.png └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | .eslintcache 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | # OSX 31 | .DS_Store 32 | 33 | # App packaged 34 | release 35 | app/main.prod.js 36 | app/main.prod.js.map 37 | app/renderer.prod.js 38 | app/renderer.prod.js.map 39 | app/style.css 40 | app/style.css.map 41 | dist 42 | dll 43 | main.js 44 | main.js.map 45 | 46 | .idea 47 | npm-debug.log.* 48 | __snapshots__ 49 | 50 | # Package.json 51 | package.json 52 | .travis.yml 53 | *.css.d.ts 54 | *.sass.d.ts 55 | *.scss.d.ts 56 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'erb/typescript', 3 | rules: { 4 | 'import/no-extraneous-dependencies': 'off', 5 | 'global-require': 'off', 6 | 'no-console': 'off', 7 | '@typescript-eslint/no-use-before-define': 'off', 8 | 'react/prefer-stateless-function': 'off', 9 | 'no-shadow': 'off', 10 | 'react/jsx-props-no-spreading': 'off', 11 | 'react/sort-comp': 'off', 12 | 'promise/always-return': 'off' 13 | }, 14 | settings: { 15 | 'import/resolver': { 16 | node: {}, 17 | webpack: { 18 | config: require.resolve('./configs/webpack.config.eslint.js') 19 | } 20 | } 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.png binary 3 | *.jpg binary 4 | *.jpeg binary 5 | *.ico binary 6 | *.icns binary 7 | *.node binary 8 | *.png binary 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/react,node,macos 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=react,node,macos 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | .com.apple.timemachine.donotpresent 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | ### Node ### 33 | # Logs 34 | logs 35 | *.log 36 | npm-debug.log* 37 | yarn-debug.log* 38 | yarn-error.log* 39 | lerna-debug.log* 40 | 41 | # Diagnostic reports (https://nodejs.org/api/report.html) 42 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 43 | 44 | # Runtime data 45 | pids 46 | *.pid 47 | *.seed 48 | *.pid.lock 49 | 50 | # Directory for instrumented libs generated by jscoverage/JSCover 51 | lib-cov 52 | 53 | # Coverage directory used by tools like istanbul 54 | coverage 55 | *.lcov 56 | 57 | # nyc test coverage 58 | .nyc_output 59 | 60 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 61 | .grunt 62 | 63 | # Bower dependency directory (https://bower.io/) 64 | bower_components 65 | 66 | # node-waf configuration 67 | .lock-wscript 68 | 69 | # Compiled binary addons (https://nodejs.org/api/addons.html) 70 | build/Release 71 | 72 | # Dependency directories 73 | node_modules/ 74 | jspm_packages/ 75 | 76 | # TypeScript v1 declaration files 77 | typings/ 78 | 79 | # TypeScript cache 80 | *.tsbuildinfo 81 | 82 | # Optional npm cache directory 83 | .npm 84 | 85 | # Optional eslint cache 86 | .eslintcache 87 | 88 | # Microbundle cache 89 | .rpt2_cache/ 90 | .rts2_cache_cjs/ 91 | .rts2_cache_es/ 92 | .rts2_cache_umd/ 93 | 94 | # Optional REPL history 95 | .node_repl_history 96 | 97 | # Output of 'npm pack' 98 | *.tgz 99 | 100 | # Yarn Integrity file 101 | .yarn-integrity 102 | 103 | # dotenv environment variables file 104 | .env 105 | .env.test 106 | 107 | # parcel-bundler cache (https://parceljs.org/) 108 | .cache 109 | 110 | # Next.js build output 111 | .next 112 | 113 | # Nuxt.js build / generate output 114 | .nuxt 115 | dist 116 | 117 | # Gatsby files 118 | .cache/ 119 | # Comment in the public line in if your project uses Gatsby and not Next.js 120 | # https://nextjs.org/blog/next-9-1#public-directory-support 121 | # public 122 | 123 | # vuepress build output 124 | .vuepress/dist 125 | 126 | # Serverless directories 127 | .serverless/ 128 | 129 | # FuseBox cache 130 | .fusebox/ 131 | 132 | # DynamoDB Local files 133 | .dynamodb/ 134 | 135 | # TernJS port file 136 | .tern-port 137 | 138 | # Stores VSCode versions used for testing VSCode extensions 139 | .vscode-test 140 | 141 | ### react ### 142 | .DS_* 143 | **/*.backup.* 144 | **/*.back.* 145 | 146 | node_modules 147 | 148 | *.sublime* 149 | 150 | psd 151 | thumb 152 | sketch 153 | 154 | ### Others ### 155 | yarn.lock 156 | package-lock.json 157 | app/yarn.lock 158 | app/package-lock.json 159 | release 160 | app/main.prod.js 161 | app/main.prod.js.map 162 | app/renderer.prod.js 163 | app/renderer.prod.js.map 164 | app/style.css 165 | app/style.css.map 166 | dll 167 | main.js 168 | main.js.map 169 | *.node 170 | 171 | .idea 172 | npm-debug.log.* 173 | *.css.d.ts 174 | *.sass.d.ts 175 | *.scss.d.ts 176 | 177 | # End of https://www.toptal.com/developers/gitignore/api/react,node,macos 178 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rousan Ali 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Demo on macOS 3 |
4 |
5 |

6 | 7 | # ElectronWithRust 8 | 9 | An example file transfer app showing how to create a very performant `Electron` app with `Rust` and [`Tokio.rs`](https://tokio.rs/). 10 | 11 | ## Install 12 | 13 | Please download the app binaries from the [releases](https://github.com/rousan/electron-with-rust/releases/tag/v1.0.0) page. 14 | 15 | ## Contributing 16 | 17 | Your PRs and suggestions are always welcome. 18 | -------------------------------------------------------------------------------- /app/app.dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ElectronWithRust 6 | 7 | 8 |
9 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/app.global.css: -------------------------------------------------------------------------------- 1 | @import '~@fortawesome/fontawesome-free/css/all.css'; 2 | html, body { 3 | margin: 0; 4 | padding: 0; 5 | width: 100%; 6 | height: 100%; 7 | font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif; 8 | font-size: 12px; 9 | box-sizing: border-box; 10 | } 11 | 12 | *, *::after, *::before { 13 | -webkit-user-select: none; 14 | -webkit-user-drag: none; 15 | -webkit-app-region: no-drag; 16 | } 17 | 18 | #root { 19 | width: 100%; 20 | height: 100%; 21 | box-sizing: border-box; 22 | } 23 | -------------------------------------------------------------------------------- /app/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rousan/electron-with-rust/92ccebb45b7f0d3998cfab926bc7bc69a4cadcbf/app/app.icns -------------------------------------------------------------------------------- /app/app.prod.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ElectronWithRust 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/components/FileItem/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rousan/electron-with-rust/92ccebb45b7f0d3998cfab926bc7bc69a4cadcbf/app/components/FileItem/index.css -------------------------------------------------------------------------------- /app/components/FileItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Progress, Card, Row, Col, Button, Icon } from 'antd'; 3 | import prettyBytes from 'pretty-bytes'; 4 | import is from 'electron-is'; 5 | import { shell, remote } from 'electron'; 6 | import './index.css'; 7 | import { FileStatus } from '../../types'; 8 | 9 | const { app } = remote; 10 | 11 | type FileItemProps = { 12 | file: { name: string; size: number }; 13 | status: FileStatus; 14 | }; 15 | 16 | type FileItemState = { 17 | fileIcon: string | null; 18 | }; 19 | 20 | class FileItem extends React.Component { 21 | static async getFileIconUrl(name: string): Promise { 22 | const img = await app.getFileIcon(name, { 23 | size: 'normal' 24 | }); 25 | return img.toDataURL(); 26 | } 27 | 28 | constructor(props: FileItemProps) { 29 | super(props); 30 | 31 | this.state = { 32 | fileIcon: null 33 | }; 34 | } 35 | 36 | componentDidMount() { 37 | const { file } = this.props; 38 | 39 | FileItem.getFileIconUrl(file.name) 40 | .then(url => { 41 | this.setState({ 42 | fileIcon: url 43 | }); 44 | }) 45 | .catch(err => { 46 | console.log(err); 47 | }); 48 | } 49 | 50 | render() { 51 | const { file, status } = this.props; 52 | const { fileIcon } = this.state; 53 | 54 | let statusElem; 55 | switch (status.type) { 56 | case 'connecting': { 57 | statusElem = ( 58 |
59 | 60 | Please wait, connecting to 61 | {` ${status.ip}:${status.port} `} 62 |
63 | ); 64 | break; 65 | } 66 | case 'progress': { 67 | statusElem = ( 68 |
69 | 77 |
78 | ); 79 | break; 80 | } 81 | case 'complete': { 82 | let label; 83 | if (is.macOS()) { 84 | label = 'Show in Finder'; 85 | } else if (is.windows()) { 86 | label = 'Show in File Explorer'; 87 | } else { 88 | label = 'Show File Location'; 89 | } 90 | 91 | statusElem = ( 92 |
93 | 96 |
97 | ); 98 | break; 99 | } 100 | case 'error': { 101 | statusElem =
{status.msg}
; 102 | break; 103 | } 104 | default: 105 | } 106 | 107 | return ( 108 |
109 | 110 | 111 | 112 |
113 | {fileIcon !== null ? ( 114 | File Icon 115 | ) : ( 116 | 117 | )} 118 |
119 | 120 | 121 |
122 |
123 | 124 | 125 |
{file.name}
126 | 127 | 128 |
{prettyBytes(file.size)}
129 | 130 |
131 |
132 |
{statusElem}
133 |
134 | 135 |
136 |
137 |
138 | ); 139 | } 140 | } 141 | 142 | export default FileItem; 143 | -------------------------------------------------------------------------------- /app/components/ReceiveFileItem/index.css: -------------------------------------------------------------------------------- 1 | .receive-file-item { 2 | margin-bottom: 15px; 3 | } 4 | -------------------------------------------------------------------------------- /app/components/ReceiveFileItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './index.css'; 3 | import { FileStatus, ReceiveFileStatus } from '../../types'; 4 | import FileItem from '../FileItem'; 5 | 6 | type ReceiveFileItemProps = { 7 | refId: string; 8 | file: { name: string; size: number }; 9 | from: { ip: string; port: number }; 10 | status: ReceiveFileStatus; 11 | }; 12 | 13 | type ReceiveFileItemState = {}; 14 | 15 | class ReceiveFileItem extends React.Component { 16 | render() { 17 | const { file, status } = this.props; 18 | 19 | let fileStatus: FileStatus | undefined; 20 | switch (status.type) { 21 | case 'progress': { 22 | fileStatus = { 23 | type: 'progress', 24 | progress: status.progress 25 | }; 26 | break; 27 | } 28 | case 'complete': { 29 | fileStatus = { 30 | type: 'complete', 31 | filePath: status.outputPath 32 | }; 33 | break; 34 | } 35 | case 'error': { 36 | fileStatus = { 37 | type: 'error', 38 | msg: status.msg 39 | }; 40 | break; 41 | } 42 | default: 43 | } 44 | 45 | return ( 46 |
47 | 48 |
49 | ); 50 | } 51 | } 52 | 53 | export default ReceiveFileItem; 54 | -------------------------------------------------------------------------------- /app/components/SendFileItem/index.css: -------------------------------------------------------------------------------- 1 | .send-file-item { 2 | margin-bottom: 15px; 3 | } 4 | -------------------------------------------------------------------------------- /app/components/SendFileItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './index.css'; 3 | import { SendFileStatus, FileStatus } from '../../types'; 4 | import FileItem from '../FileItem'; 5 | 6 | type SendFileItemProps = { 7 | refId: string; 8 | file: { path: string; name: string; size: number }; 9 | to: { ip: string; port: number }; 10 | status: SendFileStatus; 11 | }; 12 | 13 | type SendFileItemState = {}; 14 | 15 | class SendFileItem extends React.Component { 16 | render() { 17 | const { file, to, status } = this.props; 18 | 19 | let fileStatus: FileStatus | undefined; 20 | switch (status.type) { 21 | case 'connecting': { 22 | fileStatus = { 23 | type: 'connecting', 24 | ip: to.ip, 25 | port: to.port 26 | }; 27 | break; 28 | } 29 | case 'progress': { 30 | fileStatus = { 31 | type: 'progress', 32 | progress: status.progress 33 | }; 34 | break; 35 | } 36 | case 'complete': { 37 | fileStatus = { 38 | type: 'complete', 39 | filePath: file.path 40 | }; 41 | break; 42 | } 43 | case 'error': { 44 | fileStatus = { 45 | type: 'error', 46 | msg: status.msg 47 | }; 48 | break; 49 | } 50 | default: 51 | } 52 | 53 | return ( 54 |
55 | 56 |
57 | ); 58 | } 59 | } 60 | 61 | export default SendFileItem; 62 | -------------------------------------------------------------------------------- /app/containers/App/index.css: -------------------------------------------------------------------------------- 1 | .app { 2 | width: 100%; 3 | height: 100%; 4 | } -------------------------------------------------------------------------------- /app/containers/App/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route } from 'react-router-dom'; 3 | import { hot } from 'react-hot-loader/root'; 4 | import './index.css'; 5 | import Home from '../Home'; 6 | 7 | function App() { 8 | return ( 9 |
10 | 11 | 12 | 13 |
14 | ); 15 | } 16 | 17 | let appComp; 18 | if (process.env.NODE_ENV === 'development') { 19 | appComp = hot(App); 20 | } else { 21 | appComp = App; 22 | } 23 | 24 | const Comp = appComp; 25 | export default Comp; 26 | -------------------------------------------------------------------------------- /app/containers/Home/index.css: -------------------------------------------------------------------------------- 1 | .home { 2 | width: 100%; 3 | height: 100%; 4 | box-sizing: border-box; 5 | padding: 20px; 6 | } 7 | 8 | .home .tab-send .send-file-box { 9 | margin-bottom: 15px; 10 | } 11 | 12 | .home .tab-send .send-files-list-wrapper { 13 | max-height: 200px; 14 | overflow-y: auto; 15 | } 16 | 17 | .home .tab-receive .receive-files-list-wrapper { 18 | max-height: 520px; 19 | overflow-y: auto; 20 | } 21 | 22 | .home .tab-send .ant-upload-list { 23 | max-height: 100px; 24 | overflow-y: auto; 25 | } 26 | -------------------------------------------------------------------------------- /app/containers/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent } from 'react'; 2 | import { Tabs, Upload, Icon, Input, Button, Divider, message, Empty, InputNumber } from 'antd'; 3 | import { remote } from 'electron'; 4 | import './index.css'; 5 | import { startTokioRuntime, startServer, sendFile, genRefId, getFileMeta } from '../../native'; 6 | import ReceiveFileItem from '../../components/ReceiveFileItem'; 7 | import SendFileItem from '../../components/SendFileItem'; 8 | import { ReceiveFileStatus, SendFileStatus } from '../../types'; 9 | import { isDev } from '../../utils'; 10 | 11 | const { TabPane } = Tabs; 12 | const { Dragger } = Upload; 13 | const { Group: ButtonGroup } = Button; 14 | const SERVER_PORT = 45670; 15 | const { Group: InputGroup } = Input; 16 | 17 | type TabType = 'send' | 'receive'; 18 | 19 | type HomeProps = {}; 20 | 21 | type HomeState = { 22 | activeTab: TabType; 23 | selectedSendFiles: File[]; 24 | recipientIP: string; 25 | recipientPort: number | undefined; 26 | sendFiles: { 27 | [key: string]: { 28 | refId: string; 29 | file: { path: string; name: string; size: number }; 30 | to: { ip: string; port: number }; 31 | status: SendFileStatus; 32 | }; 33 | }; 34 | receiveFiles: { 35 | [key: string]: { 36 | refId: string; 37 | file: { name: string; size: number }; 38 | from: { ip: string; port: number }; 39 | status: ReceiveFileStatus; 40 | }; 41 | }; 42 | }; 43 | 44 | class Home extends React.Component { 45 | constructor(props: HomeProps) { 46 | super(props); 47 | 48 | this.state = { 49 | activeTab: 'send', 50 | selectedSendFiles: [], 51 | recipientIP: isDev() ? '127.0.0.1' : '', 52 | recipientPort: undefined, 53 | sendFiles: {}, 54 | receiveFiles: {} 55 | }; 56 | } 57 | 58 | componentDidMount() { 59 | startTokioRuntime(); 60 | startServer({ 61 | port: SERVER_PORT, 62 | receiveFilesDir: isDev() 63 | ? `${remote.app.getPath('desktop')}/electron-with-rust-outputs` 64 | : remote.app.getPath('downloads'), 65 | onStart: () => { 66 | console.log('Server Started'); 67 | }, 68 | onReceiveFileStart: (refId, from, file) => { 69 | const newFile = { 70 | refId, 71 | file, 72 | from, 73 | status: { 74 | type: 'progress', 75 | progress: 0 76 | } as ReceiveFileStatus 77 | }; 78 | 79 | this.setState(prevState => { 80 | return { 81 | activeTab: 'receive', 82 | receiveFiles: { [refId]: newFile, ...prevState.receiveFiles } 83 | }; 84 | }); 85 | }, 86 | onReceiveFileProgress: (refId, progress) => { 87 | this.updateReceiveFileStatus(refId, { type: 'progress', progress }); 88 | }, 89 | onReceiveFileComplete: (refId, outputPath) => { 90 | this.updateReceiveFileStatus(refId, { type: 'complete', outputPath }); 91 | }, 92 | onReceiveFileError: (refId, msg) => { 93 | this.updateReceiveFileStatus(refId, { type: 'error', msg }); 94 | }, 95 | onServerError: msg => { 96 | console.log('onServerError', msg); 97 | } 98 | }); 99 | } 100 | 101 | updateSendFileStatus(refId: string, status: SendFileStatus) { 102 | this.setState(prevState => { 103 | const sendFiles = { ...prevState.sendFiles }; 104 | 105 | if (sendFiles[refId]) { 106 | sendFiles[refId].status = status; 107 | } else if (status.type === 'error') { 108 | message.error(status.msg); 109 | } 110 | 111 | return { 112 | sendFiles 113 | }; 114 | }); 115 | } 116 | 117 | updateReceiveFileStatus(refId: string, status: ReceiveFileStatus) { 118 | this.setState(prevState => { 119 | const receiveFiles = { ...prevState.receiveFiles }; 120 | 121 | if (receiveFiles[refId]) { 122 | receiveFiles[refId].status = status; 123 | } else if (status.type === 'error') { 124 | message.error(status.msg); 125 | } 126 | 127 | return { 128 | receiveFiles 129 | }; 130 | }); 131 | } 132 | 133 | queueSingleFileToSend(refId: string, filePath: string, ip: string, port: number) { 134 | const fileMeta = getFileMeta(filePath); 135 | 136 | const newFile = { 137 | refId, 138 | file: { path: filePath, name: fileMeta.name, size: fileMeta.size }, 139 | to: { ip, port }, 140 | status: { type: 'connecting' } as SendFileStatus 141 | }; 142 | 143 | this.setState( 144 | prevState => { 145 | return { 146 | sendFiles: { [refId]: newFile, ...prevState.sendFiles } 147 | }; 148 | }, 149 | () => { 150 | sendFile({ 151 | refId, 152 | ip, 153 | port, 154 | filePath, 155 | onSendFileStart: refId => { 156 | this.updateSendFileStatus(refId, { type: 'progress', progress: 0 }); 157 | }, 158 | onSendFileProgress: (refId, progress) => { 159 | this.updateSendFileStatus(refId, { type: 'progress', progress }); 160 | }, 161 | onSendFileComplete: refId => { 162 | this.updateSendFileStatus(refId, { type: 'complete' }); 163 | }, 164 | onSendFileError: (refId, msg) => { 165 | this.updateSendFileStatus(refId, { type: 'error', msg }); 166 | } 167 | }); 168 | } 169 | ); 170 | } 171 | 172 | onTabChange(key: TabType) { 173 | this.setState({ 174 | activeTab: key 175 | }); 176 | } 177 | 178 | onChangeRecipientIP(evt: ChangeEvent) { 179 | this.setState({ 180 | recipientIP: evt.target.value.trim() 181 | }); 182 | } 183 | 184 | onChangeRecipientPort(value: number | undefined) { 185 | this.setState({ 186 | recipientPort: value 187 | }); 188 | } 189 | 190 | onClickQueueSendButton() { 191 | const { selectedSendFiles, recipientIP, recipientPort } = this.state; 192 | 193 | if (selectedSendFiles.length === 0) { 194 | message.error('Please select file to send'); 195 | return; 196 | } 197 | 198 | if (recipientIP === '') { 199 | message.error('Please provide recipient IP address'); 200 | return; 201 | } 202 | 203 | selectedSendFiles.forEach(file => { 204 | setImmediate(() => { 205 | const refId = genRefId(); 206 | const filePath = file.path; 207 | this.queueSingleFileToSend(refId, filePath, recipientIP, recipientPort || SERVER_PORT); 208 | }); 209 | }); 210 | } 211 | 212 | onClickResetSendButton() { 213 | this.setState({ 214 | selectedSendFiles: [] 215 | }); 216 | } 217 | 218 | onClickClearAllSendFilesButton() { 219 | this.setState({ 220 | sendFiles: {} 221 | }); 222 | } 223 | 224 | onClickClearAllReceiveFilesButton() { 225 | this.setState({ 226 | receiveFiles: {} 227 | }); 228 | } 229 | 230 | render() { 231 | const { activeTab, selectedSendFiles, recipientIP, recipientPort, sendFiles, receiveFiles } = this.state; 232 | 233 | const draggerProps = { 234 | multiple: true, 235 | 236 | onRemove: (file: File) => { 237 | this.setState(prevState => { 238 | const selectedSendFiles = [...prevState.selectedSendFiles]; 239 | 240 | const index = selectedSendFiles.indexOf(file); 241 | selectedSendFiles.splice(index, 1); 242 | 243 | return { 244 | selectedSendFiles 245 | }; 246 | }); 247 | }, 248 | 249 | beforeUpload: (file: File) => { 250 | this.setState(prevState => { 251 | return { 252 | selectedSendFiles: [...prevState.selectedSendFiles, file] 253 | }; 254 | }); 255 | return false; 256 | }, 257 | fileList: selectedSendFiles 258 | }; 259 | 260 | return ( 261 |
262 | { 265 | this.onTabChange(key as TabType); 266 | }} 267 | animated={false} 268 | size="small" 269 | > 270 | 271 |
272 |
273 | 274 |

275 | 276 |

277 |

Click or drag file to this area to send

278 |

Sending Bulk files are also supported

279 |
280 |
281 | 282 | { 289 | this.onChangeRecipientIP(evt); 290 | }} 291 | onPressEnter={() => this.onClickQueueSendButton()} 292 | style={{ width: '83%' }} 293 | /> 294 | { 299 | this.onChangeRecipientPort(value); 300 | }} 301 | /> 302 | 303 |
304 |
305 | 306 | 309 | 312 | 313 |
314 |
315 | {Object.keys(sendFiles).length > 0 ? ( 316 |
317 | 318 |
319 | 322 |
323 |
324 | {Object.keys(sendFiles).map(key => { 325 | const { refId, file, to, status } = sendFiles[key]; 326 | return ; 327 | })} 328 |
329 |
330 | ) : null} 331 |
332 |
333 | 334 |
335 | {Object.keys(receiveFiles).length > 0 ? ( 336 |
337 | 340 |
341 | ) : null} 342 |
343 | {Object.keys(receiveFiles).length > 0 ? ( 344 | Object.keys(receiveFiles).map(key => { 345 | const { refId, file, from, status } = receiveFiles[key]; 346 | return ; 347 | }) 348 | ) : ( 349 |
350 | 351 |
352 | )} 353 |
354 |
355 |
356 |
357 |
358 | ); 359 | } 360 | } 361 | 362 | export default Home; 363 | -------------------------------------------------------------------------------- /app/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { HashRouter } from 'react-router-dom'; 4 | import './app.global.css'; 5 | import App from './containers/App'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | -------------------------------------------------------------------------------- /app/main.prod.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2010 LearnBoost 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /*! 18 | * Copyright (c) 2015, Salesforce.com, Inc. 19 | * All rights reserved. 20 | * 21 | * Redistribution and use in source and binary forms, with or without 22 | * modification, are permitted provided that the following conditions are met: 23 | * 24 | * 1. Redistributions of source code must retain the above copyright notice, 25 | * this list of conditions and the following disclaimer. 26 | * 27 | * 2. Redistributions in binary form must reproduce the above copyright notice, 28 | * this list of conditions and the following disclaimer in the documentation 29 | * and/or other materials provided with the distribution. 30 | * 31 | * 3. Neither the name of Salesforce.com nor the names of its contributors may 32 | * be used to endorse or promote products derived from this software without 33 | * specific prior written permission. 34 | * 35 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 36 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 37 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 38 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 39 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 40 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 41 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 42 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 43 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 44 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 45 | * POSSIBILITY OF SUCH DAMAGE. 46 | */ 47 | 48 | /*! 49 | * Copyright (c) 2018, Salesforce.com, Inc. 50 | * All rights reserved. 51 | * 52 | * Redistribution and use in source and binary forms, with or without 53 | * modification, are permitted provided that the following conditions are met: 54 | * 55 | * 1. Redistributions of source code must retain the above copyright notice, 56 | * this list of conditions and the following disclaimer. 57 | * 58 | * 2. Redistributions in binary form must reproduce the above copyright notice, 59 | * this list of conditions and the following disclaimer in the documentation 60 | * and/or other materials provided with the distribution. 61 | * 62 | * 3. Neither the name of Salesforce.com nor the names of its contributors may 63 | * be used to endorse or promote products derived from this software without 64 | * specific prior written permission. 65 | * 66 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 67 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 68 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 69 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 70 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 71 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 72 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 73 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 74 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 75 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 76 | * POSSIBILITY OF SUCH DAMAGE. 77 | */ 78 | 79 | /*! 80 | * mime-db 81 | * Copyright(c) 2014 Jonathan Ong 82 | * MIT Licensed 83 | */ 84 | 85 | /*! 86 | * mime-types 87 | * Copyright(c) 2014 Jonathan Ong 88 | * Copyright(c) 2015 Douglas Christopher Wilson 89 | * MIT Licensed 90 | */ 91 | 92 | /*! safe-buffer. MIT License. Feross Aboukhadijeh */ 93 | 94 | /** @license URI.js v4.2.1 (c) 2011 Gary Court. License: http://github.com/garycourt/uri-js */ 95 | -------------------------------------------------------------------------------- /app/main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | import installExtension, { REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer'; 3 | import electronDebug from 'electron-debug'; 4 | import { isProd, isDev } from './utils'; 5 | 6 | let mainWindow: BrowserWindow | null = null; 7 | 8 | (async () => { 9 | if (isProd()) { 10 | require('source-map-support').install(); 11 | } 12 | 13 | if (isDev() || process.env.DEBUG_PROD === 'true') { 14 | electronDebug(); 15 | } 16 | 17 | await app.whenReady(); 18 | 19 | if (isDev() || process.env.DEBUG_PROD === 'true') { 20 | await installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS], !!process.env.UPGRADE_EXTENSIONS); 21 | } 22 | 23 | await createMainWindow(); 24 | 25 | app.on('window-all-closed', () => { 26 | // Respect the OSX convention of having the application in memory even 27 | // after all windows have been closed. 28 | if (process.platform !== 'darwin') { 29 | app.quit(); 30 | } 31 | }); 32 | 33 | app.on('activate', async () => { 34 | if (BrowserWindow.getAllWindows().length === 0) { 35 | await createMainWindow(); 36 | } 37 | }); 38 | })().catch(err => { 39 | console.error(err); 40 | }); 41 | 42 | async function createMainWindow() { 43 | mainWindow = new BrowserWindow({ 44 | show: false, 45 | width: 550, 46 | height: 700, 47 | webPreferences: { 48 | nodeIntegration: true 49 | }, 50 | resizable: false, 51 | center: true 52 | }); 53 | 54 | if (isProd()) { 55 | await mainWindow.loadURL(`file://${__dirname}/app.prod.html`); 56 | } else { 57 | await mainWindow.loadURL(`file://${__dirname}/app.dev.html`); 58 | } 59 | 60 | mainWindow.show(); 61 | mainWindow.focus(); 62 | 63 | mainWindow.on('closed', () => { 64 | mainWindow = null; 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /app/native.ts: -------------------------------------------------------------------------------- 1 | const { 2 | nativeStartTokioRuntime, 3 | nativeShutdownTokioRuntime, 4 | nativeStartServer, 5 | nativeSendFile, 6 | nativeGenRefId, 7 | nativeGetFileMeta 8 | } = require('./native.node'); 9 | 10 | export type ServerConfig = { 11 | port: number; 12 | receiveFilesDir: string; 13 | onStart: () => void; 14 | onReceiveFileStart: (refId: string, from: { ip: string; port: number }, file: { name: string; size: number }) => void; 15 | onReceiveFileProgress: (refId: string, progress: number) => void; 16 | onReceiveFileComplete: (refId: string, outputPath: string) => void; 17 | onReceiveFileError: (refId: string, msg: string) => void; 18 | onServerError: (msg: string) => void; 19 | }; 20 | 21 | export type SendFileConfig = { 22 | refId: string; 23 | ip: string; 24 | port: number; 25 | filePath: string; 26 | onSendFileStart: (refId: string) => void; 27 | onSendFileProgress: (refId: string, progress: number) => void; 28 | onSendFileComplete: (refId: string) => void; 29 | onSendFileError: (refId: string, msg: string) => void; 30 | }; 31 | 32 | export function startTokioRuntime() { 33 | nativeStartTokioRuntime(); 34 | } 35 | 36 | export function shutdownTokioRuntime() { 37 | nativeShutdownTokioRuntime(); 38 | } 39 | 40 | export function startServer(config: ServerConfig) { 41 | nativeStartServer(config); 42 | } 43 | 44 | export function sendFile(config: SendFileConfig) { 45 | nativeSendFile(config); 46 | } 47 | 48 | export function genRefId(): string { 49 | return nativeGenRefId(); 50 | } 51 | 52 | export function getFileMeta(path: string): { name: string; size: number } { 53 | return nativeGetFileMeta(path); 54 | } 55 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-with-rust", 3 | "productName": "ElectronWithRust", 4 | "version": "1.0.0", 5 | "description": "An example file transfer app showing how to create a very performant Electron app with Rust and Tokio.rs", 6 | "main": "./main.prod.js", 7 | "author": "Rousan Ali (https://rousan.io)", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/rousan/electron-with-rust.git" 11 | }, 12 | "homepage": "https://github.com/rousan/electron-with-rust", 13 | "scripts": { 14 | "electron-rebuild": "node -r ../internals/scripts/babel-register.js ../internals/scripts/electron-rebuild.js", 15 | "postinstall": "yarn electron-rebuild" 16 | }, 17 | "license": "MIT", 18 | "dependencies": {} 19 | } 20 | -------------------------------------------------------------------------------- /app/types/index.ts: -------------------------------------------------------------------------------- 1 | export type ReceiveFileStatus = 2 | | { type: 'progress'; progress: number } 3 | | { type: 'complete'; outputPath: string } 4 | | { type: 'error'; msg: string }; 5 | 6 | export type SendFileStatus = 7 | | { type: 'connecting' } 8 | | { type: 'progress'; progress: number } 9 | | { type: 'complete' } 10 | | { type: 'error'; msg: string }; 11 | 12 | export type FileStatus = 13 | | { type: 'connecting'; ip: string; port: number } 14 | | { type: 'progress'; progress: number } 15 | | { type: 'complete'; filePath: string } 16 | | { type: 'error'; msg: string }; 17 | -------------------------------------------------------------------------------- /app/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function isDev(): boolean { 2 | return process.env.NODE_ENV === 'development'; 3 | } 4 | 5 | export function isProd(): boolean { 6 | return process.env.NODE_ENV === 'production'; 7 | } 8 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off, import/no-extraneous-dependencies: off */ 2 | 3 | const developmentEnvironments = ['development', 'test']; 4 | 5 | const developmentPlugins = [require('react-hot-loader/babel')]; 6 | 7 | const productionPlugins = [ 8 | require('babel-plugin-dev-expression'), 9 | require('@babel/plugin-transform-react-constant-elements'), 10 | require('@babel/plugin-transform-react-inline-elements'), 11 | require('babel-plugin-transform-react-remove-prop-types') 12 | ]; 13 | 14 | module.exports = api => { 15 | const development = api.env(developmentEnvironments); 16 | 17 | return { 18 | presets: [ 19 | require('@babel/preset-env'), 20 | require('@babel/preset-typescript'), 21 | [require('@babel/preset-react'), { development }] 22 | ], 23 | plugins: [ 24 | require('@babel/plugin-proposal-function-bind'), 25 | require('@babel/plugin-proposal-export-default-from'), 26 | require('@babel/plugin-proposal-logical-assignment-operators'), 27 | [require('@babel/plugin-proposal-optional-chaining'), { loose: false }], 28 | [ 29 | require('@babel/plugin-proposal-pipeline-operator'), 30 | { proposal: 'minimal' } 31 | ], 32 | [ 33 | require('@babel/plugin-proposal-nullish-coalescing-operator'), 34 | { loose: false } 35 | ], 36 | require('@babel/plugin-proposal-do-expressions'), 37 | [require('@babel/plugin-proposal-decorators'), { legacy: true }], 38 | require('@babel/plugin-proposal-function-sent'), 39 | require('@babel/plugin-proposal-export-namespace-from'), 40 | require('@babel/plugin-proposal-numeric-separator'), 41 | require('@babel/plugin-proposal-throw-expressions'), 42 | require('@babel/plugin-syntax-dynamic-import'), 43 | require('@babel/plugin-syntax-import-meta'), 44 | [require('@babel/plugin-proposal-class-properties'), { loose: true }], 45 | require('@babel/plugin-proposal-json-strings'), 46 | [ 47 | require('babel-plugin-import'), 48 | { libraryName: 'antd', libraryDirectory: 'es', style: 'css' } 49 | ], 50 | ...(development ? developmentPlugins : productionPlugins) 51 | ] 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /configs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /configs/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import { dependencies as externals } from '../app/package.json'; 4 | 5 | export default { 6 | externals: [...Object.keys(externals || {}), './native.node'], 7 | 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.tsx?$/, 12 | exclude: /node_modules/, 13 | use: { 14 | loader: 'babel-loader', 15 | options: { 16 | cacheDirectory: true 17 | } 18 | } 19 | } 20 | ] 21 | }, 22 | 23 | output: { 24 | path: path.join(__dirname, '..', 'app'), 25 | libraryTarget: 'commonjs2' 26 | }, 27 | 28 | resolve: { 29 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 30 | modules: [path.join(__dirname, '..', 'app'), 'node_modules'] 31 | }, 32 | 33 | plugins: [ 34 | new webpack.EnvironmentPlugin({ 35 | NODE_ENV: 'production' 36 | }), 37 | 38 | new webpack.NamedModulesPlugin() 39 | ] 40 | }; 41 | -------------------------------------------------------------------------------- /configs/webpack.config.eslint.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | 3 | require('@babel/register'); 4 | 5 | module.exports = require('./webpack.config.renderer.dev.babel').default; 6 | -------------------------------------------------------------------------------- /configs/webpack.config.main.prod.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import merge from 'webpack-merge'; 4 | import TerserPlugin from 'terser-webpack-plugin'; 5 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 6 | import baseConfig from './webpack.config.base'; 7 | import checkNodeEnv from '../internals/scripts/check-node-env'; 8 | import deleteSourceMaps from '../internals/scripts/delete-source-maps'; 9 | 10 | checkNodeEnv('production'); 11 | deleteSourceMaps(); 12 | 13 | export default merge.smart(baseConfig, { 14 | devtool: process.env.DEBUG_PROD === 'true' ? 'source-map' : 'none', 15 | 16 | mode: 'production', 17 | 18 | target: 'electron-main', 19 | 20 | entry: './app/main.ts', 21 | 22 | output: { 23 | path: path.join(__dirname, '..'), 24 | filename: './app/main.prod.js' 25 | }, 26 | 27 | optimization: { 28 | minimizer: [ 29 | new TerserPlugin({ 30 | parallel: true, 31 | sourceMap: true, 32 | cache: true 33 | }) 34 | ] 35 | }, 36 | 37 | plugins: [ 38 | new BundleAnalyzerPlugin({ 39 | analyzerMode: 40 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', 41 | openAnalyzer: process.env.OPEN_ANALYZER === 'true' 42 | }), 43 | 44 | /** 45 | * Create global constants which can be configured at compile time. 46 | * 47 | * Useful for allowing different behaviour between development builds and 48 | * release builds 49 | * 50 | * NODE_ENV should be production so that modules do not perform certain 51 | * development checks 52 | */ 53 | new webpack.EnvironmentPlugin({ 54 | NODE_ENV: 'production', 55 | DEBUG_PROD: false, 56 | START_MINIMIZED: false 57 | }) 58 | ], 59 | 60 | /** 61 | * Disables webpack processing of __dirname and __filename. 62 | * If you run the bundle in node.js it falls back to these values of node.js. 63 | * https://github.com/webpack/webpack/issues/2010 64 | */ 65 | node: { 66 | __dirname: false, 67 | __filename: false 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /configs/webpack.config.renderer.dev.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import webpack from 'webpack'; 4 | import chalk from 'chalk'; 5 | import merge from 'webpack-merge'; 6 | import { spawn, execSync } from 'child_process'; 7 | import baseConfig from './webpack.config.base'; 8 | import checkNodeEnv from '../internals/scripts/check-node-env'; 9 | 10 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 11 | // at the dev webpack config is not accidentally run in a production environment 12 | if (process.env.NODE_ENV === 'production') { 13 | checkNodeEnv('development'); 14 | } 15 | 16 | const port = process.env.PORT || 1212; 17 | const publicPath = `http://localhost:${port}/dist`; 18 | const dll = path.join(__dirname, '..', 'dll'); 19 | const manifest = path.resolve(dll, 'renderer.json'); 20 | const requiredByDLLConfig = module.parent.filename.includes( 21 | 'webpack.config.renderer.dev.dll' 22 | ); 23 | 24 | if (!requiredByDLLConfig && !(fs.existsSync(dll) && fs.existsSync(manifest))) { 25 | console.log( 26 | chalk.black.bgYellow.bold( 27 | 'The DLL files are missing. Sit back while we build them for you with "yarn build-dll"' 28 | ) 29 | ); 30 | execSync('yarn build-dll'); 31 | } 32 | 33 | export default merge.smart(baseConfig, { 34 | devtool: 'inline-source-map', 35 | 36 | mode: 'development', 37 | 38 | target: 'electron-renderer', 39 | 40 | entry: [ 41 | ...(process.env.PLAIN_HMR ? [] : ['react-hot-loader/patch']), 42 | `webpack-dev-server/client?http://localhost:${port}/`, 43 | 'webpack/hot/only-dev-server', 44 | require.resolve('../app/index.tsx') 45 | ], 46 | 47 | output: { 48 | publicPath: `http://localhost:${port}/dist/`, 49 | filename: 'renderer.dev.js' 50 | }, 51 | 52 | module: { 53 | rules: [ 54 | { 55 | test: /\.global\.css$/, 56 | use: [ 57 | { 58 | loader: 'style-loader' 59 | }, 60 | { 61 | loader: 'css-loader', 62 | options: { 63 | sourceMap: true 64 | } 65 | } 66 | ] 67 | }, 68 | { 69 | test: /^((?!\.global).)*\.css$/, 70 | use: [ 71 | { 72 | loader: 'style-loader' 73 | }, 74 | { 75 | loader: 'css-loader', 76 | options: { 77 | sourceMap: true, 78 | importLoaders: 1 79 | } 80 | } 81 | ] 82 | }, 83 | { 84 | test: /\.global\.(scss|sass)$/, 85 | use: [ 86 | { 87 | loader: 'style-loader' 88 | }, 89 | { 90 | loader: 'css-loader', 91 | options: { 92 | sourceMap: true 93 | } 94 | }, 95 | { 96 | loader: 'sass-loader' 97 | } 98 | ] 99 | }, 100 | { 101 | test: /^((?!\.global).)*\.(scss|sass)$/, 102 | use: [ 103 | { 104 | loader: 'style-loader' 105 | }, 106 | { 107 | loader: 'css-loader', 108 | options: { 109 | sourceMap: true, 110 | importLoaders: 1 111 | } 112 | }, 113 | { 114 | loader: 'sass-loader' 115 | } 116 | ] 117 | }, 118 | { 119 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 120 | use: { 121 | loader: 'url-loader', 122 | options: { 123 | limit: 10000, 124 | mimetype: 'application/font-woff' 125 | } 126 | } 127 | }, 128 | { 129 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 130 | use: { 131 | loader: 'url-loader', 132 | options: { 133 | limit: 10000, 134 | mimetype: 'application/font-woff' 135 | } 136 | } 137 | }, 138 | { 139 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 140 | use: { 141 | loader: 'url-loader', 142 | options: { 143 | limit: 10000, 144 | mimetype: 'application/octet-stream' 145 | } 146 | } 147 | }, 148 | { 149 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 150 | use: 'file-loader' 151 | }, 152 | { 153 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 154 | use: { 155 | loader: 'url-loader', 156 | options: { 157 | limit: 10000, 158 | mimetype: 'image/svg+xml' 159 | } 160 | } 161 | }, 162 | { 163 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, 164 | use: 'url-loader' 165 | } 166 | ] 167 | }, 168 | resolve: { 169 | alias: { 170 | 'react-dom': '@hot-loader/react-dom' 171 | } 172 | }, 173 | plugins: [ 174 | requiredByDLLConfig 175 | ? null 176 | : new webpack.DllReferencePlugin({ 177 | context: path.join(__dirname, '..', 'dll'), 178 | manifest: require(manifest), 179 | sourceType: 'var' 180 | }), 181 | 182 | new webpack.HotModuleReplacementPlugin({ 183 | multiStep: true 184 | }), 185 | 186 | new webpack.NoEmitOnErrorsPlugin(), 187 | 188 | /** 189 | * Create global constants which can be configured at compile time. 190 | * 191 | * Useful for allowing different behaviour between development builds and 192 | * release builds 193 | * 194 | * NODE_ENV should be production so that modules do not perform certain 195 | * development checks 196 | * 197 | * By default, use 'development' as NODE_ENV. This can be overridden with 198 | * 'staging', for example, by changing the ENV variables in the npm scripts 199 | */ 200 | new webpack.EnvironmentPlugin({ 201 | NODE_ENV: 'development' 202 | }), 203 | 204 | new webpack.LoaderOptionsPlugin({ 205 | debug: true 206 | }) 207 | ], 208 | 209 | node: { 210 | __dirname: false, 211 | __filename: false 212 | }, 213 | 214 | devServer: { 215 | port, 216 | publicPath, 217 | compress: true, 218 | noInfo: true, 219 | stats: 'errors-only', 220 | inline: true, 221 | lazy: false, 222 | hot: true, 223 | headers: { 'Access-Control-Allow-Origin': '*' }, 224 | contentBase: path.join(__dirname, 'dist'), 225 | watchOptions: { 226 | aggregateTimeout: 300, 227 | ignored: /node_modules/, 228 | poll: 100 229 | }, 230 | historyApiFallback: { 231 | verbose: true, 232 | disableDotRule: false 233 | }, 234 | before() { 235 | if (process.env.START_HOT) { 236 | console.log('Starting Main Process...'); 237 | spawn('npm', ['run', 'start-main-dev'], { 238 | shell: true, 239 | env: process.env, 240 | stdio: 'inherit' 241 | }) 242 | .on('close', code => process.exit(code)) 243 | .on('error', spawnError => console.error(spawnError)); 244 | } 245 | } 246 | } 247 | }); 248 | -------------------------------------------------------------------------------- /configs/webpack.config.renderer.dev.dll.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import path from 'path'; 3 | import merge from 'webpack-merge'; 4 | import baseConfig from './webpack.config.base'; 5 | import { dependencies } from '../package.json'; 6 | import checkNodeEnv from '../internals/scripts/check-node-env'; 7 | 8 | checkNodeEnv('development'); 9 | 10 | const dist = path.join(__dirname, '..', 'dll'); 11 | 12 | export default merge.smart(baseConfig, { 13 | context: path.join(__dirname, '..'), 14 | 15 | devtool: 'eval', 16 | 17 | mode: 'development', 18 | 19 | target: 'electron-renderer', 20 | 21 | externals: ['fsevents', 'crypto-browserify'], 22 | 23 | module: require('./webpack.config.renderer.dev.babel').default.module, 24 | 25 | entry: { 26 | renderer: Object.keys(dependencies || {}) 27 | }, 28 | 29 | output: { 30 | library: 'renderer', 31 | path: dist, 32 | filename: '[name].dev.dll.js', 33 | libraryTarget: 'var' 34 | }, 35 | 36 | plugins: [ 37 | new webpack.DllPlugin({ 38 | path: path.join(dist, '[name].json'), 39 | name: '[name]' 40 | }), 41 | 42 | /** 43 | * Create global constants which can be configured at compile time. 44 | * 45 | * Useful for allowing different behaviour between development builds and 46 | * release builds 47 | * 48 | * NODE_ENV should be production so that modules do not perform certain 49 | * development checks 50 | */ 51 | new webpack.EnvironmentPlugin({ 52 | NODE_ENV: 'development' 53 | }), 54 | 55 | new webpack.LoaderOptionsPlugin({ 56 | debug: true, 57 | options: { 58 | context: path.join(__dirname, '..', 'app'), 59 | output: { 60 | path: path.join(__dirname, '..', 'dll') 61 | } 62 | } 63 | }) 64 | ] 65 | }); 66 | -------------------------------------------------------------------------------- /configs/webpack.config.renderer.prod.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 4 | import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin'; 5 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 6 | import merge from 'webpack-merge'; 7 | import TerserPlugin from 'terser-webpack-plugin'; 8 | import baseConfig from './webpack.config.base'; 9 | import checkNodeEnv from '../internals/scripts/check-node-env'; 10 | import deleteSourceMaps from '../internals/scripts/delete-source-maps'; 11 | 12 | checkNodeEnv('production'); 13 | deleteSourceMaps(); 14 | 15 | export default merge.smart(baseConfig, { 16 | devtool: process.env.DEBUG_PROD === 'true' ? 'source-map' : 'none', 17 | 18 | mode: 'production', 19 | 20 | target: 'electron-preload', 21 | 22 | entry: path.join(__dirname, '..', 'app/index.tsx'), 23 | 24 | output: { 25 | path: path.join(__dirname, '..', 'app/dist'), 26 | publicPath: './dist/', 27 | filename: 'renderer.prod.js' 28 | }, 29 | 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.global\.css$/, 34 | use: [ 35 | { 36 | loader: MiniCssExtractPlugin.loader, 37 | options: { 38 | publicPath: './' 39 | } 40 | }, 41 | { 42 | loader: 'css-loader', 43 | options: { 44 | sourceMap: true 45 | } 46 | } 47 | ] 48 | }, 49 | { 50 | test: /^((?!\.global).)*\.css$/, 51 | use: [ 52 | { 53 | loader: MiniCssExtractPlugin.loader 54 | }, 55 | { 56 | loader: 'css-loader', 57 | options: { 58 | sourceMap: true 59 | } 60 | } 61 | ] 62 | }, 63 | { 64 | test: /\.global\.(scss|sass)$/, 65 | use: [ 66 | { 67 | loader: MiniCssExtractPlugin.loader 68 | }, 69 | { 70 | loader: 'css-loader', 71 | options: { 72 | sourceMap: true, 73 | importLoaders: 1 74 | } 75 | }, 76 | { 77 | loader: 'sass-loader', 78 | options: { 79 | sourceMap: true 80 | } 81 | } 82 | ] 83 | }, 84 | { 85 | test: /^((?!\.global).)*\.(scss|sass)$/, 86 | use: [ 87 | { 88 | loader: MiniCssExtractPlugin.loader 89 | }, 90 | { 91 | loader: 'css-loader', 92 | options: { 93 | importLoaders: 1, 94 | sourceMap: true 95 | } 96 | }, 97 | { 98 | loader: 'sass-loader', 99 | options: { 100 | sourceMap: true 101 | } 102 | } 103 | ] 104 | }, 105 | { 106 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 107 | use: { 108 | loader: 'url-loader', 109 | options: { 110 | limit: 10000, 111 | mimetype: 'application/font-woff' 112 | } 113 | } 114 | }, 115 | { 116 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 117 | use: { 118 | loader: 'url-loader', 119 | options: { 120 | limit: 10000, 121 | mimetype: 'application/font-woff' 122 | } 123 | } 124 | }, 125 | { 126 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 127 | use: { 128 | loader: 'url-loader', 129 | options: { 130 | limit: 10000, 131 | mimetype: 'application/octet-stream' 132 | } 133 | } 134 | }, 135 | { 136 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 137 | use: 'file-loader' 138 | }, 139 | { 140 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 141 | use: { 142 | loader: 'url-loader', 143 | options: { 144 | limit: 10000, 145 | mimetype: 'image/svg+xml' 146 | } 147 | } 148 | }, 149 | { 150 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, 151 | use: 'url-loader' 152 | } 153 | ] 154 | }, 155 | 156 | optimization: { 157 | minimizer: [ 158 | new TerserPlugin({ 159 | parallel: true, 160 | sourceMap: true, 161 | cache: true 162 | }), 163 | new OptimizeCSSAssetsPlugin({ 164 | cssProcessorOptions: { 165 | map: { 166 | inline: false, 167 | annotation: true 168 | } 169 | } 170 | }) 171 | ] 172 | }, 173 | 174 | plugins: [ 175 | /** 176 | * Create global constants which can be configured at compile time. 177 | * 178 | * Useful for allowing different behaviour between development builds and 179 | * release builds 180 | * 181 | * NODE_ENV should be production so that modules do not perform certain 182 | * development checks 183 | */ 184 | new webpack.EnvironmentPlugin({ 185 | NODE_ENV: 'production', 186 | DEBUG_PROD: false 187 | }), 188 | 189 | new MiniCssExtractPlugin({ 190 | filename: 'style.css' 191 | }), 192 | 193 | new BundleAnalyzerPlugin({ 194 | analyzerMode: 195 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', 196 | openAnalyzer: process.env.OPEN_ANALYZER === 'true' 197 | }) 198 | ] 199 | }); 200 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rousan/electron-with-rust/92ccebb45b7f0d3998cfab926bc7bc69a4cadcbf/demo.png -------------------------------------------------------------------------------- /internals/scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off", 6 | "import/no-extraneous-dependencies": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /internals/scripts/babel-register.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | require('@babel/register')({ 4 | extensions: ['.es6', '.es', '.jsx', '.js', '.mjs', '.ts', '.tsx'], 5 | cwd: path.join(__dirname, '..', '..') 6 | }); 7 | -------------------------------------------------------------------------------- /internals/scripts/check-builds-exist.js: -------------------------------------------------------------------------------- 1 | // Check if the renderer and main bundles are built 2 | import path from 'path'; 3 | import chalk from 'chalk'; 4 | import fs from 'fs'; 5 | 6 | const mainPath = path.join(__dirname, '..', '..', 'app', 'main.prod.js'); 7 | const rendererPath = path.join( 8 | __dirname, 9 | '..', 10 | '..', 11 | 'app', 12 | 'dist', 13 | 'renderer.prod.js' 14 | ); 15 | 16 | if (!fs.existsSync(mainPath)) { 17 | throw new Error( 18 | chalk.whiteBright.bgRed.bold( 19 | 'The main process is not built yet. Build it by running "yarn build-main"' 20 | ) 21 | ); 22 | } 23 | 24 | if (!fs.existsSync(rendererPath)) { 25 | throw new Error( 26 | chalk.whiteBright.bgRed.bold( 27 | 'The renderer process is not built yet. Build it by running "yarn build-renderer"' 28 | ) 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /internals/scripts/check-native-dep.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import chalk from 'chalk'; 3 | import { execSync } from 'child_process'; 4 | import { dependencies } from '../../package.json'; 5 | 6 | if (dependencies) { 7 | const dependenciesKeys = Object.keys(dependencies); 8 | const nativeDeps = fs 9 | .readdirSync('node_modules') 10 | .filter(folder => fs.existsSync(`node_modules/${folder}/binding.gyp`)); 11 | try { 12 | // Find the reason for why the dependency is installed. If it is installed 13 | // because of a devDependency then that is okay. Warn when it is installed 14 | // because of a dependency 15 | const { dependencies: dependenciesObject } = JSON.parse( 16 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString() 17 | ); 18 | const rootDependencies = Object.keys(dependenciesObject); 19 | const filteredRootDependencies = rootDependencies.filter(rootDependency => 20 | dependenciesKeys.includes(rootDependency) 21 | ); 22 | if (filteredRootDependencies.length > 0) { 23 | const plural = filteredRootDependencies.length > 1; 24 | console.log(` 25 | ${chalk.whiteBright.bgYellow.bold( 26 | 'Webpack does not work with native dependencies.' 27 | )} 28 | ${chalk.bold(filteredRootDependencies.join(', '))} ${ 29 | plural ? 'are native dependencies' : 'is a native dependency' 30 | } and should be installed inside of the "./app" folder. 31 | First, uninstall the packages from "./package.json": 32 | ${chalk.whiteBright.bgGreen.bold('yarn remove your-package')} 33 | ${chalk.bold( 34 | 'Then, instead of installing the package to the root "./package.json":' 35 | )} 36 | ${chalk.whiteBright.bgRed.bold('yarn add your-package')} 37 | ${chalk.bold('Install the package to "./app/package.json"')} 38 | ${chalk.whiteBright.bgGreen.bold('cd ./app && yarn add your-package')} 39 | Read more about native dependencies at: 40 | ${chalk.bold( 41 | 'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure' 42 | )} 43 | `); 44 | process.exit(1); 45 | } 46 | } catch (e) { 47 | console.log('Native dependencies could not be checked'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internals/scripts/check-node-env.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export default function checkNodeEnv(expectedEnv) { 4 | if (!expectedEnv) { 5 | throw new Error('"expectedEnv" not set'); 6 | } 7 | 8 | if (process.env.NODE_ENV !== expectedEnv) { 9 | console.log( 10 | chalk.whiteBright.bgRed.bold( 11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config` 12 | ) 13 | ); 14 | process.exit(2); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internals/scripts/check-port-in-use.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import detectPort from 'detect-port'; 3 | 4 | const port = process.env.PORT || '1212'; 5 | 6 | detectPort(port, (err, availablePort) => { 7 | if (port !== String(availablePort)) { 8 | throw new Error( 9 | chalk.whiteBright.bgRed.bold( 10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 yarn dev` 11 | ) 12 | ); 13 | } else { 14 | process.exit(0); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /internals/scripts/check-yarn.js: -------------------------------------------------------------------------------- 1 | if (!/yarn\.js$/.test(process.env.npm_execpath || '')) { 2 | console.warn( 3 | "\u001b[33mYou don't seem to be using yarn. This could produce unexpected results.\u001b[39m" 4 | ); 5 | } 6 | -------------------------------------------------------------------------------- /internals/scripts/delete-source-maps.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import rimraf from 'rimraf'; 3 | 4 | export default function deleteSourceMaps() { 5 | rimraf.sync(path.join(__dirname, '../../app/dist/*.js.map')); 6 | rimraf.sync(path.join(__dirname, '../../app/*.js.map')); 7 | } 8 | -------------------------------------------------------------------------------- /internals/scripts/electron-rebuild.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { execSync } from 'child_process'; 3 | import fs from 'fs'; 4 | import { dependencies } from '../../app/package.json'; 5 | 6 | const nodeModulesPath = path.join(__dirname, '..', '..', 'app', 'node_modules'); 7 | 8 | if ( 9 | Object.keys(dependencies || {}).length > 0 && 10 | fs.existsSync(nodeModulesPath) 11 | ) { 12 | const electronRebuildCmd = 13 | '../node_modules/.bin/electron-rebuild --parallel --force --types prod,dev,optional --module-dir .'; 14 | const cmd = 15 | process.platform === 'win32' 16 | ? electronRebuildCmd.replace(/\//g, '\\') 17 | : electronRebuildCmd; 18 | execSync(cmd, { 19 | cwd: path.join(__dirname, '..', '..', 'app'), 20 | stdio: 'inherit' 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /native/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,rust 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,rust 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | .com.apple.timemachine.donotpresent 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | ### Rust ### 33 | # Generated by Cargo 34 | # will have compiled files and executables 35 | /target/ 36 | 37 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 38 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 39 | Cargo.lock 40 | 41 | # These are backup files generated by rustfmt 42 | **/*.rs.bk 43 | 44 | ### Others ### 45 | /.idea 46 | /artifacts.json 47 | 48 | # End of https://www.toptal.com/developers/gitignore/api/macos,rust 49 | -------------------------------------------------------------------------------- /native/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "native-electron-with-rust" 3 | version = "0.0.0" 4 | build = "build.rs" 5 | edition = "2018" 6 | 7 | [lib] 8 | name = "nativeelectronwithrust" 9 | crate-type = ["cdylib"] 10 | 11 | [build-dependencies] 12 | neon-build = "0.4.0" 13 | 14 | [dependencies] 15 | neon = { version = "0.4.0", features = ["event-handler-api"] } 16 | neon-serde = "0.4.0" 17 | reqwest = { version = "0.10", features = ["blocking", "json"] } 18 | serde = { version = "1.0", features = ["derive"] } 19 | serde_json = "1.0" 20 | tokio = { version = "0.2", features = ["full"] } 21 | num_cpus = "1.13.0" 22 | uuid = { version = "0.8", features = ["v4"] } 23 | async-trait = "0.1" 24 | regex = "1" 25 | -------------------------------------------------------------------------------- /native/build.rs: -------------------------------------------------------------------------------- 1 | use neon_build; 2 | 3 | fn main() { 4 | neon_build::setup(); 5 | } 6 | -------------------------------------------------------------------------------- /native/rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | tab_spaces = 4 3 | -------------------------------------------------------------------------------- /native/src/error.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::fmt::{self, Debug, Display, Formatter}; 4 | 5 | pub struct Error { 6 | msg: String, 7 | } 8 | 9 | impl Error { 10 | pub fn new>(msg: M) -> Self { 11 | Error { msg: msg.into() } 12 | } 13 | } 14 | 15 | impl Display for Error { 16 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 17 | write!(f, "native-electron-with-rust::Error: {}", self.msg) 18 | } 19 | } 20 | 21 | impl Debug for Error { 22 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 23 | write!(f, "native-electron-with-rust::Error: {}", self.msg) 24 | } 25 | } 26 | 27 | impl std::error::Error for Error { 28 | fn description(&self) -> &str { 29 | self.msg.as_str() 30 | } 31 | } 32 | 33 | pub trait ErrorExt { 34 | fn wrap(self) -> Error; 35 | fn context(self, ctx: C) -> Error; 36 | } 37 | 38 | impl ErrorExt for E { 39 | fn wrap(self) -> Error { 40 | Error { msg: self.to_string() } 41 | } 42 | 43 | fn context(self, ctx: C) -> Error { 44 | let msg = format!("{}: {}", ctx, self.to_string()); 45 | Error { msg } 46 | } 47 | } 48 | 49 | pub trait ResultExt { 50 | fn wrap(self) -> Result; 51 | fn context(self, ctx: C) -> Result; 52 | fn throw<'a, C: neon::context::Context<'a>>(self, cx: &mut C) -> neon::result::NeonResult; 53 | } 54 | 55 | impl ResultExt for Result { 56 | fn wrap(self) -> Result { 57 | self.map_err(|e| e.wrap()) 58 | } 59 | 60 | fn context(self, ctx: C) -> Result { 61 | self.map_err(|e| e.context(ctx)) 62 | } 63 | 64 | fn throw<'a, C: neon::context::Context<'a>>(self, cx: &mut C) -> neon::result::NeonResult { 65 | match self { 66 | Ok(val) => Ok(val), 67 | Err(err) => cx.throw_error(err.to_string()), 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /native/src/ext/eh.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | type EventContext<'a> = neon::context::TaskContext<'a>; 4 | 5 | pub trait EventHandlerExt { 6 | fn emit(&self, args_cb: F) 7 | where 8 | F: for<'a> FnOnce(&mut EventContext<'a>) -> Vec>, 9 | F: Send + 'static; 10 | } 11 | 12 | impl EventHandlerExt for EventHandler { 13 | fn emit(&self, args_cb: F) 14 | where 15 | F: for<'a> FnOnce(&mut EventContext<'a>) -> Vec>, 16 | F: Send + 'static, 17 | { 18 | self.schedule(args_cb) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /native/src/ext/json_stream.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use async_trait::async_trait; 3 | use serde::{de::DeserializeOwned, Serialize}; 4 | 5 | const MAX_JSON_STREAM_DATA_SIZE: u64 = 1024 * 1024; 6 | 7 | #[async_trait] 8 | pub trait JsonStreamWriteExt { 9 | async fn write_json(&mut self, value: &T) -> crate::Result<()>; 10 | } 11 | 12 | #[async_trait] 13 | impl JsonStreamWriteExt for R { 14 | async fn write_json(&mut self, value: &T) -> crate::Result<()> { 15 | let json_data = 16 | serde_json::to_vec(value).context("Failed to encode the provided value to JSON for json-stream")?; 17 | 18 | self.write_u64(json_data.len() as u64) 19 | .await 20 | .context("Failed to write json-stream data byte size")?; 21 | 22 | self.write_all(&json_data) 23 | .await 24 | .context("Failed to write json-stream data")?; 25 | 26 | Ok(()) 27 | } 28 | } 29 | 30 | #[async_trait] 31 | pub trait JsonStreamReadExt { 32 | async fn read_json(&mut self) -> crate::Result; 33 | } 34 | 35 | #[async_trait] 36 | impl JsonStreamReadExt for R { 37 | async fn read_json(&mut self) -> crate::Result { 38 | let json_data_size = self 39 | .read_u64() 40 | .await 41 | .context("Failed to read json-stream data byte size")?; 42 | 43 | // Add a guard to prevent attacks which could cause a huge memory allocation. 44 | if json_data_size > MAX_JSON_STREAM_DATA_SIZE { 45 | return Err(crate::Error::new(format!( 46 | "The json-stream data size {} exceeds the maximum {} bytes", 47 | json_data_size, MAX_JSON_STREAM_DATA_SIZE 48 | ))); 49 | } 50 | 51 | let mut json_data = vec![0_u8; json_data_size as usize]; 52 | let read_len = self 53 | .read_exact(&mut json_data) 54 | .await 55 | .context("Failed to read json-stream data")?; 56 | 57 | if read_len != json_data_size as usize { 58 | return Err(crate::Error::new("Failed to read json-stream data completely")); 59 | } 60 | 61 | serde_json::from_slice::(&json_data).context("Failed to parse json-stream data as JSON") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /native/src/ext/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::eh::EventHandlerExt; 2 | pub use self::json_stream::{JsonStreamReadExt, JsonStreamWriteExt}; 3 | pub use self::object::JSObjectExt; 4 | 5 | mod eh; 6 | mod json_stream; 7 | mod object; 8 | -------------------------------------------------------------------------------- /native/src/ext/object.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use neon::object::PropertyKey; 3 | 4 | pub trait JSObjectExt { 5 | fn prop<'a, C: Context<'a>, K: PropertyKey, T: Value>(self, cx: &mut C, key: K) -> NeonResult>; 6 | fn number<'a, C: Context<'a>, K: PropertyKey>(self, cx: &mut C, key: K) -> NeonResult; 7 | fn string<'a, C: Context<'a>, K: PropertyKey>(self, cx: &mut C, key: K) -> NeonResult; 8 | fn func<'a, C: Context<'a>, K: PropertyKey>(self, cx: &mut C, key: K) -> NeonResult>; 9 | fn callback<'a, C: Context<'a>, K: PropertyKey>(self, cx: &mut C, key: K) -> NeonResult; 10 | } 11 | 12 | impl JSObjectExt for O { 13 | fn prop<'a, C: Context<'a>, K: PropertyKey, T: Value>(self, cx: &mut C, key: K) -> NeonResult> { 14 | self.get(cx, key)?.downcast_or_throw(cx) 15 | } 16 | 17 | fn number<'a, C: Context<'a>, K: PropertyKey>(self, cx: &mut C, key: K) -> NeonResult { 18 | let num: Handle = self.prop(cx, key)?; 19 | Ok(num.value()) 20 | } 21 | 22 | fn string<'a, C: Context<'a>, K: PropertyKey>(self, cx: &mut C, key: K) -> NeonResult { 23 | let string: Handle = self.prop(cx, key)?; 24 | Ok(string.value()) 25 | } 26 | 27 | fn func<'a, C: Context<'a>, K: PropertyKey>(self, cx: &mut C, key: K) -> NeonResult> { 28 | self.prop(cx, key) 29 | } 30 | 31 | fn callback<'a, C: Context<'a>, K: PropertyKey>(self, cx: &mut C, key: K) -> NeonResult { 32 | let cb = self.func(cx, key)?; 33 | let this = cx.undefined(); 34 | Ok(EventHandler::new(cx, this, cb)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /native/src/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use regex::Regex; 3 | use std::path::{Path, PathBuf}; 4 | use tokio::io::ErrorKind; 5 | use tokio::time::{Duration, Instant}; 6 | use uuid::Uuid; 7 | 8 | const STREAM_PROGRESS_CALL_THROTTLE_DURATION: Duration = Duration::from_millis(100); 9 | 10 | pub fn gen_uuid() -> String { 11 | Uuid::new_v4() 12 | .to_hyphenated() 13 | .encode_lower(&mut Uuid::encode_buffer()) 14 | .to_string() 15 | } 16 | 17 | pub async fn pipe(reader: &mut R, writer: &mut W, progress: F) -> crate::Result<()> 18 | where 19 | R: AsyncRead + Unpin + Send + Sync, 20 | W: AsyncWrite + Unpin + Send + Sync, 21 | F: Fn(u64) + Send + Sync, 22 | { 23 | let mut buf = vec![0_u8; 16 * 1024]; 24 | 25 | let mut written = 0; 26 | let mut called_progress: Option = None; 27 | loop { 28 | let len = match reader.read(&mut buf).await { 29 | Ok(0) => break, 30 | Ok(len) => len, 31 | Err(ref err) if err.kind() == ErrorKind::Interrupted => continue, 32 | Err(err) => return Err(err.context("Failed to read data from the reader while piping")), 33 | }; 34 | 35 | writer 36 | .write_all(&buf[..len]) 37 | .await 38 | .context("Failed to write data to the writer while piping")?; 39 | 40 | written += len as u64; 41 | 42 | match called_progress { 43 | Some(i) => { 44 | if i.elapsed() > STREAM_PROGRESS_CALL_THROTTLE_DURATION { 45 | progress(written); 46 | called_progress = Some(Instant::now()); 47 | } 48 | } 49 | None => { 50 | progress(written); 51 | called_progress = Some(Instant::now()); 52 | } 53 | } 54 | } 55 | 56 | progress(written); 57 | 58 | Ok(()) 59 | } 60 | 61 | pub fn generate_file_path_with_available_name(parent_path: impl AsRef, name: &str) -> crate::Result { 62 | let parent_path = parent_path.as_ref(); 63 | let ext = Path::new(name).extension().and_then(|ext| ext.to_str()); 64 | let re = Regex::new(format!(r"\.{}$", regex::escape(ext.unwrap_or(""))).as_str()) 65 | .context("Failed to create regex for generating available file name")?; 66 | 67 | let mut curr_file_path = parent_path.join(name); 68 | let mut counter = 1_u64; 69 | 70 | loop { 71 | if !curr_file_path.exists() { 72 | return Ok(curr_file_path); 73 | } 74 | 75 | match ext { 76 | Some(ext) => { 77 | let new_name = re.replace(name, format!("({}).{}", counter, ext).as_str()).to_string(); 78 | curr_file_path = parent_path.join(new_name.as_str()); 79 | } 80 | None => { 81 | curr_file_path = parent_path.join(format!("{}({})", name, counter)); 82 | } 83 | } 84 | 85 | counter += 1; 86 | } 87 | } 88 | 89 | #[cfg(test)] 90 | mod tests { 91 | use super::*; 92 | 93 | #[test] 94 | fn test_generate_file_path_with_available_name() { 95 | let parent_path = "/Users/rousan/Desktop/electron-with-rust-outputs"; 96 | 97 | println!("{:?}", generate_file_path_with_available_name(parent_path, "abc.txt")); 98 | println!( 99 | "{:?}", 100 | generate_file_path_with_available_name(parent_path, "abc.txt.txt") 101 | ); 102 | println!("{:?}", generate_file_path_with_available_name(parent_path, "abc")); 103 | println!("{:?}", generate_file_path_with_available_name(parent_path, ".abc.txt")); 104 | println!("{:?}", generate_file_path_with_available_name(parent_path, ".abc")); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /native/src/lib.rs: -------------------------------------------------------------------------------- 1 | use neon::register_module; 2 | 3 | pub use error::Error; 4 | 5 | mod error; 6 | mod ext; 7 | mod helpers; 8 | mod prelude; 9 | mod runtime; 10 | mod transfer; 11 | mod types; 12 | mod utils; 13 | 14 | pub type Result = std::result::Result; 15 | 16 | register_module!(mut cx, { 17 | cx.export_function("nativeStartTokioRuntime", runtime::start_runtime) 18 | .unwrap(); 19 | cx.export_function("nativeShutdownTokioRuntime", runtime::shutdown_runtime) 20 | .unwrap(); 21 | cx.export_function("nativeStartServer", transfer::start_server).unwrap(); 22 | cx.export_function("nativeSendFile", transfer::send_file).unwrap(); 23 | cx.export_function("nativeGenRefId", utils::gen_ref_id).unwrap(); 24 | cx.export_function("nativeGetFileMeta", utils::get_file_meta).unwrap(); 25 | 26 | Ok(()) 27 | }); 28 | -------------------------------------------------------------------------------- /native/src/prelude/mod.rs: -------------------------------------------------------------------------------- 1 | pub use crate::error::{Error, ErrorExt, ResultExt}; 2 | pub use crate::ext::{EventHandlerExt, JSObjectExt, JsonStreamReadExt, JsonStreamWriteExt}; 3 | pub use neon::prelude::*; 4 | pub use tokio::prelude::*; 5 | pub use tokio::stream::StreamExt; 6 | -------------------------------------------------------------------------------- /native/src/runtime/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use std::future::Future; 3 | use tokio::runtime::Runtime; 4 | use tokio::task::JoinHandle; 5 | 6 | static mut RUNTIME: Option = None; 7 | 8 | pub fn start_runtime(mut cx: FunctionContext) -> JsResult { 9 | unsafe { 10 | if RUNTIME.is_some() { 11 | return Ok(cx.undefined()); 12 | } 13 | 14 | RUNTIME.replace( 15 | tokio::runtime::Builder::new() 16 | .threaded_scheduler() 17 | .core_threads(runtime_core_threads_count()) 18 | .on_thread_start(|| { 19 | println!("Tokio worker thread started: {:?}", std::thread::current().id()); 20 | }) 21 | .on_thread_stop(|| { 22 | println!("Tokio worker thread stopped: {:?}", std::thread::current().id()); 23 | }) 24 | .enable_io() 25 | .enable_time() 26 | .build() 27 | .unwrap(), 28 | ); 29 | } 30 | 31 | Ok(cx.undefined()) 32 | } 33 | 34 | pub fn shutdown_runtime(mut cx: FunctionContext) -> JsResult { 35 | if let Some(runtime) = unsafe { RUNTIME.take() } { 36 | println!("Shutdown tokio runtime started"); 37 | runtime.shutdown_timeout(tokio::time::Duration::from_secs(1)); 38 | println!("Shutdown tokio runtime done"); 39 | } 40 | 41 | Ok(cx.undefined()) 42 | } 43 | 44 | pub fn spawn(task: T) -> JoinHandle 45 | where 46 | T: Future + Send + 'static, 47 | T::Output: Send + 'static, 48 | { 49 | unsafe { RUNTIME.as_ref().unwrap().spawn(task) } 50 | } 51 | 52 | fn runtime_core_threads_count() -> usize { 53 | num_cpus::get().min(4).max(2) 54 | } 55 | -------------------------------------------------------------------------------- /native/src/transfer/client/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::helpers; 2 | use crate::prelude::*; 3 | use crate::runtime; 4 | use crate::types::TransferFileMeta; 5 | use std::path::PathBuf; 6 | use std::sync::Arc; 7 | use tokio::fs::File; 8 | use tokio::net::TcpStream; 9 | 10 | pub fn send_file(mut cx: FunctionContext) -> JsResult { 11 | let config = cx.argument::(0)?; 12 | 13 | let ref_id = config.string(&mut cx, "refId")?; 14 | let ip = config.string(&mut cx, "ip")?; 15 | let port = config.number(&mut cx, "port")?; 16 | let file_path = config.string(&mut cx, "filePath")?; 17 | let on_send_file_start = config.callback(&mut cx, "onSendFileStart")?; 18 | let on_send_file_progress = config.callback(&mut cx, "onSendFileProgress")?; 19 | let on_send_file_complete = config.callback(&mut cx, "onSendFileComplete")?; 20 | let on_send_file_error = config.callback(&mut cx, "onSendFileError")?; 21 | 22 | runtime::spawn(async move { 23 | let ref_id = Arc::new(ref_id); 24 | 25 | let result = transfer_file( 26 | ref_id.clone(), 27 | ip, 28 | port as u16, 29 | file_path, 30 | on_send_file_start, 31 | on_send_file_progress, 32 | on_send_file_complete, 33 | ) 34 | .await; 35 | 36 | if let Err(err) = result { 37 | on_send_file_error 38 | .emit(move |cx| vec![cx.string(ref_id.as_str()).upcast(), cx.string(err.to_string()).upcast()]) 39 | } 40 | }); 41 | 42 | Ok(cx.undefined()) 43 | } 44 | 45 | async fn transfer_file( 46 | ref_id: Arc, 47 | ip: String, 48 | port: u16, 49 | file_path: String, 50 | on_send_file_start: EventHandler, 51 | on_send_file_progress: EventHandler, 52 | on_send_file_complete: EventHandler, 53 | ) -> crate::Result<()> { 54 | let mut socket = TcpStream::connect((ip.as_str(), port)) 55 | .await 56 | .context(format!("Failed to connect with the recipient server: {}:{}", ip, port))?; 57 | 58 | let file_path = PathBuf::from(file_path) 59 | .canonicalize() 60 | .context("Selected source file does not exist")?; 61 | 62 | let name = file_path.file_name().and_then(|name| name.to_str()).unwrap_or("file"); 63 | let size = file_path 64 | .metadata() 65 | .context("Failed to get metadata for the selected source file")? 66 | .len(); 67 | 68 | let cloned_ref_id = ref_id.clone(); 69 | on_send_file_start.emit(move |cx| vec![cx.string(cloned_ref_id.as_str()).upcast()]); 70 | 71 | let transfer_meta = TransferFileMeta { 72 | name: name.to_owned(), 73 | size, 74 | }; 75 | 76 | socket 77 | .write_json(&transfer_meta) 78 | .await 79 | .context("Failed to write transfer-meta JSON for the selected source file")?; 80 | 81 | let mut source_file = File::open(file_path.as_path()) 82 | .await 83 | .context("Failed to open the selected source file")?; 84 | 85 | helpers::pipe(&mut source_file, &mut socket, |progress| { 86 | let cloned_ref_id = ref_id.clone(); 87 | on_send_file_progress.emit(move |cx| { 88 | vec![ 89 | cx.string(cloned_ref_id.as_str()).upcast(), 90 | cx.number(progress as f64).upcast(), 91 | ] 92 | }); 93 | }) 94 | .await 95 | .context("Failed to pipe selected source file data to socket")?; 96 | 97 | let cloned_ref_id = ref_id.clone(); 98 | on_send_file_complete.emit(move |cx| vec![cx.string(cloned_ref_id.as_str()).upcast()]); 99 | 100 | Ok(()) 101 | } 102 | -------------------------------------------------------------------------------- /native/src/transfer/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::client::send_file; 2 | pub use self::server::start_server; 3 | 4 | mod client; 5 | mod server; 6 | -------------------------------------------------------------------------------- /native/src/transfer/server/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::helpers; 2 | use crate::prelude::*; 3 | use crate::runtime; 4 | use crate::types::TransferFileMeta; 5 | use serde_json::json; 6 | use std::net::IpAddr; 7 | use std::sync::Arc; 8 | use tokio::fs::OpenOptions; 9 | use tokio::net::{TcpListener, TcpStream}; 10 | 11 | pub fn start_server(mut cx: FunctionContext) -> JsResult { 12 | let config = cx.argument::(0)?; 13 | 14 | let port = config.number(&mut cx, "port")?; 15 | let receive_files_dir = config.string(&mut cx, "receiveFilesDir")?; 16 | let on_start = config.callback(&mut cx, "onStart")?; 17 | let on_receive_file_start = config.callback(&mut cx, "onReceiveFileStart")?; 18 | let on_receive_file_progress = config.callback(&mut cx, "onReceiveFileProgress")?; 19 | let on_receive_file_complete = config.callback(&mut cx, "onReceiveFileComplete")?; 20 | let on_receive_file_error = config.callback(&mut cx, "onReceiveFileError")?; 21 | let on_server_error = config.callback(&mut cx, "onServerError")?; 22 | 23 | runtime::spawn(async move { 24 | let result = spawn_tcp_server( 25 | port as u16, 26 | Arc::new(receive_files_dir), 27 | on_start, 28 | on_receive_file_start, 29 | on_receive_file_progress, 30 | on_receive_file_complete, 31 | on_receive_file_error, 32 | ) 33 | .await; 34 | 35 | if let Err(err) = result { 36 | // It means server is shutdown for some reason. 37 | on_server_error.emit(move |cx| vec![cx.string(err.to_string()).upcast()]) 38 | } 39 | }); 40 | 41 | Ok(cx.undefined()) 42 | } 43 | 44 | async fn spawn_tcp_server( 45 | port: u16, 46 | receive_files_dir: Arc, 47 | on_start: EventHandler, 48 | on_receive_file_start: EventHandler, 49 | on_receive_file_progress: EventHandler, 50 | on_receive_file_complete: EventHandler, 51 | on_receive_file_error: EventHandler, 52 | ) -> crate::Result<()> { 53 | let mut server = TcpListener::bind((IpAddr::from([0, 0, 0, 0]), port)) 54 | .await 55 | .context(format!("Failed to bind the server to port: {}", port))?; 56 | 57 | on_start.emit(move |_| vec![]); 58 | 59 | while let Some(socket) = server.next().await { 60 | let socket = socket.context("Failed to accept new connection")?; 61 | 62 | let ref_id = Arc::new(helpers::gen_uuid()); 63 | let receive_files_dir = receive_files_dir.clone(); 64 | let on_receive_file_start = on_receive_file_start.clone(); 65 | let on_receive_file_progress = on_receive_file_progress.clone(); 66 | let on_receive_file_complete = on_receive_file_complete.clone(); 67 | let on_receive_file_error = on_receive_file_error.clone(); 68 | 69 | tokio::spawn(async move { 70 | let result = handle_socket( 71 | ref_id.clone(), 72 | receive_files_dir, 73 | socket, 74 | on_receive_file_start, 75 | on_receive_file_progress, 76 | on_receive_file_complete, 77 | ) 78 | .await; 79 | 80 | if let Err(err) = result { 81 | println!("{}", err); 82 | on_receive_file_error 83 | .emit(move |cx| vec![cx.string(ref_id.as_str()).upcast(), cx.string(err.to_string()).upcast()]) 84 | } 85 | }); 86 | } 87 | 88 | Ok(()) 89 | } 90 | 91 | async fn handle_socket( 92 | ref_id: Arc, 93 | receive_files_dir: Arc, 94 | mut socket: TcpStream, 95 | on_receive_file_start: EventHandler, 96 | on_receive_file_progress: EventHandler, 97 | on_receive_file_complete: EventHandler, 98 | ) -> crate::Result<()> { 99 | let peer_addr = socket 100 | .peer_addr() 101 | .context("Failed to get peer remote address while receiving file")?; 102 | 103 | let transfer_meta = socket 104 | .read_json::() 105 | .await 106 | .context("Failed to read transfer-meta json")?; 107 | 108 | let cloned_ref_id = ref_id.clone(); 109 | let from_meta = json!({ 110 | "ip": peer_addr.ip().to_string(), 111 | "port": peer_addr.port() 112 | }); 113 | let file_mata = json!({ 114 | "name": transfer_meta.name.as_str(), 115 | "size": transfer_meta.size 116 | }); 117 | on_receive_file_start.emit(move |cx| { 118 | vec![ 119 | cx.string(cloned_ref_id.as_str()).upcast(), 120 | neon_serde::to_value(cx, &from_meta).unwrap(), 121 | neon_serde::to_value(cx, &file_mata).unwrap(), 122 | ] 123 | }); 124 | 125 | let output_file_path = 126 | helpers::generate_file_path_with_available_name(receive_files_dir.as_str(), transfer_meta.name.as_str()) 127 | .context("Failed to generate a new file path for the receiving file")?; 128 | 129 | let mut out_file = OpenOptions::new() 130 | .write(true) 131 | .create(true) 132 | .open(output_file_path.as_path()) 133 | .await 134 | .context("Failed to create a output file for the receiving file")?; 135 | 136 | helpers::pipe(&mut socket, &mut out_file, |progress| { 137 | let cloned_ref_id = ref_id.clone(); 138 | on_receive_file_progress.emit(move |cx| { 139 | vec![ 140 | cx.string(cloned_ref_id.as_str()).upcast(), 141 | cx.number(progress as f64).upcast(), 142 | ] 143 | }); 144 | }) 145 | .await 146 | .context("Failed to pipe socket data to the output file")?; 147 | 148 | let cloned_ref_id = ref_id.clone(); 149 | on_receive_file_complete.emit(move |cx| { 150 | vec![ 151 | cx.string(cloned_ref_id.as_str()).upcast(), 152 | cx.string(output_file_path.to_str().unwrap()).upcast(), 153 | ] 154 | }); 155 | 156 | Ok(()) 157 | } 158 | -------------------------------------------------------------------------------- /native/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug)] 4 | pub struct TransferFileMeta { 5 | pub name: String, 6 | pub size: u64, 7 | } 8 | -------------------------------------------------------------------------------- /native/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::helpers; 2 | use crate::prelude::*; 3 | use serde_json::json; 4 | use std::path::PathBuf; 5 | 6 | pub fn gen_ref_id(mut cx: FunctionContext) -> JsResult { 7 | Ok(cx.string(helpers::gen_uuid())) 8 | } 9 | 10 | pub fn get_file_meta(mut cx: FunctionContext) -> JsResult { 11 | let file_path = PathBuf::from(cx.argument::(0)?.value()); 12 | 13 | let meta_data = std::fs::metadata(file_path.as_path()) 14 | .context("Failed to get file metadata") 15 | .throw(&mut cx)?; 16 | 17 | let file_name = file_path.file_name().and_then(|name| name.to_str()).unwrap_or(""); 18 | 19 | let meta_obj = neon_serde::to_value( 20 | &mut cx, 21 | &json!({ 22 | "name": file_name, 23 | "size": meta_data.len() 24 | }), 25 | ) 26 | .wrap() 27 | .throw(&mut cx)? 28 | .downcast_or_throw::(&mut cx)?; 29 | 30 | Ok(meta_obj) 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-with-rust", 3 | "productName": "ElectronWithRust", 4 | "version": "1.0.0", 5 | "description": "An example file transfer app showing how to create a very performant Electron app with Rust and Tokio.rs", 6 | "scripts": { 7 | "build": "concurrently \"yarn build-native\" \"yarn build-main\" \"yarn build-renderer\"", 8 | "build-dll": "cross-env NODE_ENV=development webpack --config ./configs/webpack.config.renderer.dev.dll.babel.js --colors", 9 | "build-main": "cross-env NODE_ENV=production webpack --config ./configs/webpack.config.main.prod.babel.js --colors", 10 | "build-renderer": "cross-env NODE_ENV=production webpack --config ./configs/webpack.config.renderer.prod.babel.js --colors", 11 | "build-native": "electron-build-env neon build --release && cp-cli native/index.node app/native.node", 12 | "build-native:dev": "electron-build-env neon build --release && cp-cli native/index.node app/native.node", 13 | "native:dev": "nodemon --ext rs --watch ./native/src --exec \"yarn build-native:dev\"", 14 | "electron:dev": "yarn build-native:dev && cross-env START_HOT=1 node -r @babel/register ./internals/scripts/check-port-in-use.js && cross-env START_HOT=1 yarn start-renderer-dev", 15 | "electron-rebuild": "electron-rebuild --parallel --force --types prod,dev,optional --module-dir app", 16 | "lint": "cross-env NODE_ENV=development eslint . --cache --ext .js,.jsx,.ts,.tsx", 17 | "lint-fix": "yarn --silent lint --fix; exit 0", 18 | "lint-styles": "stylelint --ignore-path .eslintignore '**/*.*(css|scss)' --syntax scss", 19 | "lint-styles-fix": "yarn --silent lint-styles --fix; exit 0", 20 | "package": "yarn build && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder build", 21 | "package:all": "yarn build && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder build -mwl", 22 | "package:mac": "yarn build && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder build --mac", 23 | "package:linux": "yarn build && electron-builder build --linux", 24 | "package:win": "yarn build && electron-builder build --win --x64", 25 | "postinstall": "node -r @babel/register internals/scripts/check-native-dep.js && electron-builder install-app-deps && yarn build-dll", 26 | "postlint-fix": "prettier --ignore-path .eslintignore --single-quote --write '**/*.{js,jsx,json,html,css,less,scss,yml}'", 27 | "postlint-styles-fix": "prettier --ignore-path .eslintignore --single-quote --write '**/*.{css,scss}'", 28 | "preinstall": "node ./internals/scripts/check-yarn.js", 29 | "prestart": "yarn build", 30 | "start": "cross-env NODE_ENV=production electron ./app/main.prod.js", 31 | "start-main-dev": "cross-env START_HOT=1 NODE_ENV=development electron -r ./internals/scripts/babel-register ./app/main.ts", 32 | "start-renderer-dev": "cross-env NODE_ENV=development webpack-dev-server --config configs/webpack.config.renderer.dev.babel.js" 33 | }, 34 | "build": { 35 | "productName": "ElectronWithRust", 36 | "appId": "io.rousan.electron-with-rust", 37 | "files": [ 38 | "dist/", 39 | "node_modules/", 40 | "app.prod.html", 41 | "main.prod.js", 42 | "main.prod.js.map", 43 | "package.json", 44 | "native.node" 45 | ], 46 | "dmg": { 47 | "contents": [ 48 | { 49 | "x": 130, 50 | "y": 220 51 | }, 52 | { 53 | "x": 410, 54 | "y": 220, 55 | "type": "link", 56 | "path": "/Applications" 57 | } 58 | ] 59 | }, 60 | "win": { 61 | "target": [ 62 | "nsis", 63 | "msi" 64 | ] 65 | }, 66 | "linux": { 67 | "target": [ 68 | "deb", 69 | "rpm", 70 | "AppImage" 71 | ], 72 | "category": "Development" 73 | }, 74 | "directories": { 75 | "buildResources": "resources", 76 | "output": "release" 77 | } 78 | }, 79 | "repository": { 80 | "type": "git", 81 | "url": "git+https://github.com/rousan/electron-with-rust.git" 82 | }, 83 | "author": "Rousan Ali (https://rousan.io)", 84 | "license": "MIT", 85 | "bugs": { 86 | "url": "https://github.com/rousan/electron-with-rust/issues" 87 | }, 88 | "keywords": [ 89 | "rust", 90 | "electron", 91 | "electron-app", 92 | "boilerplate", 93 | "tokio-rs" 94 | ], 95 | "homepage": "https://github.com/rousan/electron-with-rust", 96 | "devDependencies": { 97 | "@babel/core": "^7.10.2", 98 | "@babel/plugin-proposal-class-properties": "^7.10.1", 99 | "@babel/plugin-proposal-decorators": "^7.10.1", 100 | "@babel/plugin-proposal-do-expressions": "^7.10.1", 101 | "@babel/plugin-proposal-export-default-from": "^7.10.1", 102 | "@babel/plugin-proposal-export-namespace-from": "^7.10.1", 103 | "@babel/plugin-proposal-function-bind": "^7.10.1", 104 | "@babel/plugin-proposal-function-sent": "^7.10.1", 105 | "@babel/plugin-proposal-json-strings": "^7.10.1", 106 | "@babel/plugin-proposal-logical-assignment-operators": "^7.10.1", 107 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.1", 108 | "@babel/plugin-proposal-numeric-separator": "^7.10.1", 109 | "@babel/plugin-proposal-optional-chaining": "^7.10.1", 110 | "@babel/plugin-proposal-pipeline-operator": "^7.10.1", 111 | "@babel/plugin-proposal-throw-expressions": "^7.10.1", 112 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 113 | "@babel/plugin-syntax-import-meta": "^7.10.1", 114 | "@babel/plugin-transform-react-constant-elements": "^7.10.1", 115 | "@babel/plugin-transform-react-inline-elements": "^7.10.1", 116 | "@babel/preset-env": "^7.10.2", 117 | "@babel/preset-react": "^7.10.1", 118 | "@babel/preset-typescript": "^7.10.1", 119 | "@babel/register": "^7.10.1", 120 | "@types/electron-devtools-installer": "^2.2.0", 121 | "@types/enzyme": "^3.10.5", 122 | "@types/enzyme-adapter-react-16": "^1.0.6", 123 | "@types/history": "^4.7.5", 124 | "@types/node": "^12", 125 | "@types/react": "^16.9.17", 126 | "@types/react-dom": "^16.9.7", 127 | "@types/react-redux": "^7.1.6", 128 | "@types/react-router": "^5.1.7", 129 | "@types/react-router-dom": "^5.1.5", 130 | "@types/react-test-renderer": "^16.9.2", 131 | "@types/redux-logger": "^3.0.7", 132 | "@types/sinon": "^7.5.2", 133 | "@types/tapable": "^1.0.5", 134 | "@types/vfile-message": "^2.0.0", 135 | "@types/webpack": "^4.41.3", 136 | "@typescript-eslint/eslint-plugin": "^2.17.0", 137 | "@typescript-eslint/parser": "^2.17.0", 138 | "babel-core": "7.0.0-bridge.0", 139 | "babel-eslint": "^10.1.0", 140 | "babel-loader": "^8.1.0", 141 | "babel-plugin-dev-expression": "^0.2.2", 142 | "babel-plugin-import": "^1.13.0", 143 | "babel-plugin-transform-react-remove-prop-types": "^0.4.24", 144 | "browserslist-config-erb": "^0.0.1", 145 | "chalk": "^3.0.0", 146 | "concurrently": "^5.2.0", 147 | "cp-cli": "^2.0.0", 148 | "cross-env": "^7.0.0", 149 | "cross-spawn": "^7.0.1", 150 | "css-loader": "^3.4.2", 151 | "detect-port": "^1.3.0", 152 | "electron": "7.1.13", 153 | "electron-build-env": "^0.2.0", 154 | "electron-builder": "^22.3.6", 155 | "electron-devtools-installer": "^2.2.4", 156 | "electron-rebuild": "^1.10.0", 157 | "enzyme": "^3.11.0", 158 | "enzyme-adapter-react-16": "^1.15.2", 159 | "enzyme-to-json": "^3.4.4", 160 | "eslint": "^7.2.0", 161 | "eslint-config-airbnb-typescript": "^6.3.1", 162 | "eslint-config-erb": "^0.3.0", 163 | "eslint-config-prettier": "^6.11.0", 164 | "eslint-import-resolver-webpack": "^0.12.1", 165 | "eslint-plugin-compat": "^3.7.0", 166 | "eslint-plugin-import": "^2.21.2", 167 | "eslint-plugin-jest": "^23.13.2", 168 | "eslint-plugin-jsx-a11y": "6.2.3", 169 | "eslint-plugin-prettier": "^3.1.3", 170 | "eslint-plugin-promise": "^4.2.1", 171 | "eslint-plugin-react": "^7.20.0", 172 | "eslint-plugin-react-hooks": "^4.0.4", 173 | "eslint-plugin-testcafe": "^0.2.1", 174 | "fbjs-scripts": "^1.2.0", 175 | "file-loader": "^5.0.2", 176 | "identity-obj-proxy": "^3.0.0", 177 | "mini-css-extract-plugin": "^0.9.0", 178 | "neon-cli": "^0.4.0", 179 | "node-sass": "^4.13.1", 180 | "nodemon": "^2.0.4", 181 | "optimize-css-assets-webpack-plugin": "^5.0.3", 182 | "prettier": "^1.19.1", 183 | "react-test-renderer": "^16.12.0", 184 | "redux-logger": "^3.0.6", 185 | "rimraf": "^3.0.0", 186 | "sass-loader": "^8.0.2", 187 | "sinon": "^8.1.1", 188 | "spectron": "^10.0.0", 189 | "style-loader": "^1.1.3", 190 | "stylelint": "^13.0.0", 191 | "stylelint-config-prettier": "^8.0.1", 192 | "stylelint-config-standard": "^19.0.0", 193 | "terser-webpack-plugin": "^2.3.2", 194 | "testcafe": "^1.8.0", 195 | "testcafe-browser-provider-electron": "^0.0.14", 196 | "testcafe-react-selectors": "^4.0.0", 197 | "typescript": "^3.7.5", 198 | "url-loader": "^3.0.0", 199 | "webpack": "^4.41.5", 200 | "webpack-bundle-analyzer": "^3.6.0", 201 | "webpack-cli": "^3.3.10", 202 | "webpack-dev-server": "^3.10.1", 203 | "webpack-merge": "^4.2.2", 204 | "yarn": "^1.21.1" 205 | }, 206 | "dependencies": { 207 | "@fortawesome/fontawesome-free": "^5.13.0", 208 | "@hot-loader/react-dom": "^16.13.0", 209 | "antd": "^3.15.0", 210 | "connected-react-router": "^6.8.0", 211 | "core-js": "^3.6.5", 212 | "devtron": "^1.4.0", 213 | "electron-debug": "^3.0.1", 214 | "electron-is": "^3.0.0", 215 | "history": "^4.10.1", 216 | "lodash": "^4.17.15", 217 | "pretty-bytes": "^5.3.0", 218 | "react": "^16.12.0", 219 | "react-dom": "^16.12.0", 220 | "react-hot-loader": "^4.12.19", 221 | "react-redux": "^7.1.3", 222 | "react-router": "^5.1.2", 223 | "react-router-dom": "^5.1.2", 224 | "redux": "^4.0.5", 225 | "redux-thunk": "^2.3.0", 226 | "source-map-support": "^0.5.16" 227 | }, 228 | "devEngines": { 229 | "node": ">=7.x", 230 | "npm": ">=4.x", 231 | "yarn": ">=0.21.3" 232 | }, 233 | "browserslist": [ 234 | "extends browserslist-config-erb" 235 | ], 236 | "prettier": { 237 | "overrides": [ 238 | { 239 | "files": [ 240 | ".prettierrc", 241 | ".babelrc", 242 | ".eslintrc", 243 | ".stylelintrc" 244 | ], 245 | "options": { 246 | "parser": "json" 247 | } 248 | } 249 | ], 250 | "singleQuote": true, 251 | "printWidth": 120 252 | }, 253 | "stylelint": { 254 | "extends": [ 255 | "stylelint-config-standard", 256 | "stylelint-config-prettier" 257 | ] 258 | }, 259 | "renovate": { 260 | "extends": [ 261 | "bliss" 262 | ] 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rousan/electron-with-rust/92ccebb45b7f0d3998cfab926bc7bc69a4cadcbf/resources/icon.icns -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rousan/electron-with-rust/92ccebb45b7f0d3998cfab926bc7bc69a4cadcbf/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rousan/electron-with-rust/92ccebb45b7f0d3998cfab926bc7bc69a4cadcbf/resources/icon.png -------------------------------------------------------------------------------- /resources/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rousan/electron-with-rust/92ccebb45b7f0d3998cfab926bc7bc69a4cadcbf/resources/icons/1024x1024.png -------------------------------------------------------------------------------- /resources/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rousan/electron-with-rust/92ccebb45b7f0d3998cfab926bc7bc69a4cadcbf/resources/icons/128x128.png -------------------------------------------------------------------------------- /resources/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rousan/electron-with-rust/92ccebb45b7f0d3998cfab926bc7bc69a4cadcbf/resources/icons/16x16.png -------------------------------------------------------------------------------- /resources/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rousan/electron-with-rust/92ccebb45b7f0d3998cfab926bc7bc69a4cadcbf/resources/icons/24x24.png -------------------------------------------------------------------------------- /resources/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rousan/electron-with-rust/92ccebb45b7f0d3998cfab926bc7bc69a4cadcbf/resources/icons/256x256.png -------------------------------------------------------------------------------- /resources/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rousan/electron-with-rust/92ccebb45b7f0d3998cfab926bc7bc69a4cadcbf/resources/icons/32x32.png -------------------------------------------------------------------------------- /resources/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rousan/electron-with-rust/92ccebb45b7f0d3998cfab926bc7bc69a4cadcbf/resources/icons/48x48.png -------------------------------------------------------------------------------- /resources/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rousan/electron-with-rust/92ccebb45b7f0d3998cfab926bc7bc69a4cadcbf/resources/icons/512x512.png -------------------------------------------------------------------------------- /resources/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rousan/electron-with-rust/92ccebb45b7f0d3998cfab926bc7bc69a4cadcbf/resources/icons/64x64.png -------------------------------------------------------------------------------- /resources/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rousan/electron-with-rust/92ccebb45b7f0d3998cfab926bc7bc69a4cadcbf/resources/icons/96x96.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "CommonJS", 5 | "lib": [ 6 | "dom", 7 | "esnext" 8 | ], 9 | "declaration": true, 10 | "declarationMap": true, 11 | "noEmit": true, 12 | "jsx": "react", 13 | "strict": true, 14 | "pretty": true, 15 | "sourceMap": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "moduleResolution": "node", 21 | "esModuleInterop": true, 22 | "allowSyntheticDefaultImports": true, 23 | "resolveJsonModule": true, 24 | "allowJs": true 25 | }, 26 | "exclude": [ 27 | "release", 28 | "app/main.prod.js", 29 | "app/main.prod.js.map", 30 | "app/dist", 31 | "dll" 32 | ] 33 | } --------------------------------------------------------------------------------