├── .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 |
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 |
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 |

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 | }
--------------------------------------------------------------------------------