├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── .prettierrc
├── README.md
├── components.json
├── index.html
├── notes.md
├── package-lock.json
├── package.json
├── src-tauri
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── build.rs
├── capabilities
│ └── default.json
├── icons
│ ├── 128x128.png
│ ├── 128x128@2x.png
│ ├── 32x32.png
│ ├── 64x64.png
│ ├── Square107x107Logo.png
│ ├── Square142x142Logo.png
│ ├── Square150x150Logo.png
│ ├── Square284x284Logo.png
│ ├── Square30x30Logo.png
│ ├── Square310x310Logo.png
│ ├── Square44x44Logo.png
│ ├── Square71x71Logo.png
│ ├── Square89x89Logo.png
│ ├── StoreLogo.png
│ ├── android
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ └── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ ├── icon.icns
│ ├── icon.ico
│ ├── icon.png
│ └── ios
│ │ ├── AppIcon-20x20@1x.png
│ │ ├── AppIcon-20x20@2x-1.png
│ │ ├── AppIcon-20x20@2x.png
│ │ ├── AppIcon-20x20@3x.png
│ │ ├── AppIcon-29x29@1x.png
│ │ ├── AppIcon-29x29@2x-1.png
│ │ ├── AppIcon-29x29@2x.png
│ │ ├── AppIcon-29x29@3x.png
│ │ ├── AppIcon-40x40@1x.png
│ │ ├── AppIcon-40x40@2x-1.png
│ │ ├── AppIcon-40x40@2x.png
│ │ ├── AppIcon-40x40@3x.png
│ │ ├── AppIcon-512@2x.png
│ │ ├── AppIcon-60x60@2x.png
│ │ ├── AppIcon-60x60@3x.png
│ │ ├── AppIcon-76x76@1x.png
│ │ ├── AppIcon-76x76@2x.png
│ │ └── AppIcon-83.5x83.5@2x.png
├── src
│ ├── download
│ │ └── mod.rs
│ ├── events.rs
│ ├── file_operations
│ │ └── mod.rs
│ ├── files.rs
│ ├── iroh.rs
│ ├── lib.rs
│ ├── main.rs
│ ├── state
│ │ ├── mod.rs
│ │ └── user_data.rs
│ ├── theme
│ │ └── mod.rs
│ ├── ticket
│ │ └── mod.rs
│ └── utils.rs
└── tauri.conf.json
├── src
├── assets
│ ├── avatars
│ │ ├── avatar1.png
│ │ ├── avatar10.png
│ │ ├── avatar11.png
│ │ ├── avatar12.png
│ │ ├── avatar2.png
│ │ ├── avatar3.png
│ │ ├── avatar4.png
│ │ ├── avatar5.png
│ │ ├── avatar6.png
│ │ ├── avatar7.png
│ │ ├── avatar8.png
│ │ ├── avatar9.png
│ │ └── index.ts
│ └── logo.png
├── components
│ ├── animated-checkmark.tsx
│ ├── loader.tsx
│ ├── titlebar.tsx
│ ├── toggle-theme.tsx
│ └── ui
│ │ ├── button.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── input.tsx
│ │ ├── scroll-area.tsx
│ │ ├── sonner.tsx
│ │ └── tooltip.tsx
├── context
│ ├── edit-profile.tsx
│ └── theme.context.tsx
├── index.css
├── lib
│ ├── post-hog.ts
│ ├── tanstack-router.ts
│ ├── tauri
│ │ ├── api.ts
│ │ ├── events.ts
│ │ ├── index.ts
│ │ ├── types.ts
│ │ └── utils.ts
│ └── zustand.ts
├── main.tsx
├── routeTree.gen.ts
├── routes
│ ├── __root.tsx
│ ├── _pages.tsx
│ └── _pages
│ │ ├── -components
│ │ ├── progress-bar.tsx
│ │ ├── queue-container.tsx
│ │ └── queue-item.tsx
│ │ ├── edit-profile.tsx
│ │ ├── index.tsx
│ │ ├── onboard.tsx
│ │ ├── receive.tsx
│ │ └── send.tsx
├── state
│ └── appstate.tsx
├── utils
│ ├── async.ts
│ ├── fs.ts
│ ├── index.ts
│ ├── style.ts
│ ├── type-utils.ts
│ └── webview.ts
└── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: publish
2 | on:
3 | push:
4 | tags:
5 | - 'v*'
6 | jobs:
7 | publish-tauri:
8 | permissions:
9 | contents: write
10 | strategy:
11 | fail-fast: false
12 | matrix:
13 | include:
14 | - platform: 'macos-latest' # for Arm based macs
15 | args: '--target aarch64-apple-darwin'
16 |
17 | - platform: 'macos-latest' # for Intel based macs
18 | args: '--target x86_64-apple-darwin'
19 |
20 | - platform: 'ubuntu-22.04'
21 | args: ''
22 |
23 | - platform: 'windows-latest'
24 | args: ''
25 |
26 | runs-on: ${{ matrix.platform }}
27 | steps:
28 | - uses: actions/checkout@v4
29 |
30 | - name: install dependencies (ubuntu only)
31 | if: matrix.platform == 'ubuntu-22.04'
32 | run: |
33 | sudo apt-get update
34 | sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
35 |
36 | - name: setup node
37 | uses: actions/setup-node@v4
38 | with:
39 | node-version: lts/*
40 | cache: ''
41 |
42 | - name: install rust stable
43 | uses: dtolnay/rust-toolchain@stable
44 | with:
45 | # Since these targets are only used on macos runners so they can be omitted for windows and ubuntu for faster builds
46 | targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
47 |
48 | - name: Rust cache
49 | uses: swatinem/rust-cache@v2
50 | with:
51 | workspaces: './src-tauri -> target'
52 |
53 | - name: install frontend dependencies
54 | run: npm install
55 |
56 | - uses: tauri-apps/tauri-action@v0
57 | env:
58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59 | VITE_POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
60 | VITE_POSTHOG_HOST: ${{ vars.POSTHOG_HOST }}
61 | with:
62 | tagName: ${{ github.ref_name }} # This action automatically replaces \_\_VERSION\_\_ with the release version
63 | releaseName: SendIt v__VERSION__
64 | releaseBody: 'See the assets to download this version and install.'
65 | releaseDraft: true
66 | prerelease: false
67 | args: ${{ matrix.args }}
68 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 | .env
10 | bun.lock
11 |
12 | node_modules
13 | dist
14 | dist-ssr
15 | *.local
16 |
17 | # Editor directories and files
18 | .vscode/*
19 | !.vscode/extensions.json
20 | .idea
21 | .DS_Store
22 | *.suo
23 | *.ntvs*
24 | *.njsproj
25 | *.sln
26 | *.sw?
27 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "plugins": ["prettier-plugin-tailwindcss"],
4 | "singleQuote": true,
5 | "jsxSingleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SendIt
2 |
3 | A modern, secure peer-to-peer file sharing application built with Tauri and React.
4 |
5 | ## Features
6 |
7 | - Simple and intuitive user interface
8 | - Fast peer-to-peer file transfers
9 | - Cross-platform support (Windows, macOS, Linux)
10 | - No file size limits
11 | - No intermediary servers - direct device-to-device transfer
12 |
13 | ## Tech Stack
14 |
15 | - **Frontend**: React
16 | - **Backend**: Rust
17 | - **Framework**: Tauri
18 | - **Build Tool**: Vite
19 |
20 | ## Prerequisites
21 |
22 | - Node.js (v16 or higher)
23 | - Rust (latest stable)
24 | - System dependencies for Tauri (see [Tauri prerequisites](https://tauri.app/start/prerequisites))
25 |
26 | ## Development Setup
27 |
28 | 1. Clone the repository:
29 | ```bash
30 | git clone https://github.com/frstycodes/sendit.git
31 | cd sendit
32 | ```
33 |
34 | 2. Install dependencies:
35 | ```bash
36 | npm install
37 | ```
38 |
39 | 2. Run the development server:
40 | ```bash
41 | npm run tauri dev
42 | ```
43 |
44 | ## Building
45 |
46 | To create a production build:
47 |
48 | ```bash
49 | npm run tauri build
50 | ```
51 |
52 | The built applications will be available in the `src-tauri/target/release` directory.
53 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "src/index.css",
9 | "baseColor": "rose",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
22 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 | Tauri + React + Typescript
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/notes.md:
--------------------------------------------------------------------------------
1 | ## __Fixes:__
2 | - [ ] 1: Disable right click context menu.
3 | - [ ] 2: Disable __Window Resizing__.
4 | - [ ] 3: Change __App Icon__.
5 | - [ ] 4: Move the upload and download queue to __Global State__(Zustand preferred), so the UI doesn't lose its state when user navigates away.
6 | - [ ] 5: Fix __Download Progress__ not working.
7 | - [ ] 6: User should be able to __Cancel__ downloads.
8 | - [ ] 7: User should be able to __Cancel__ uploads.
9 |
10 |
11 | ## __Future Features:__
12 | - [ ] 1: Implement __Drag and Drop__ functionality.
13 | - [ ] 2: Add extensive __Icon Library__ for file icons.
14 | - [ ] 3: Allow user to __Minimize__ the app the system tray.
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "send-it",
3 | "dev": "vite",
4 | "private": true,
5 | "version": "0.4.1",
6 | "type": "module",
7 | "scripts": {
8 | "dev": "vite",
9 | "build": "tsc && vite build",
10 | "preview": "vite preview",
11 | "tauri": "tauri"
12 | },
13 | "dependencies": {
14 | "@radix-ui/react-dropdown-menu": "^2.1.6",
15 | "@radix-ui/react-scroll-area": "^1.2.3",
16 | "@radix-ui/react-slot": "^1.1.2",
17 | "@radix-ui/react-tooltip": "^1.1.8",
18 | "@tailwindcss/vite": "^4.1.4",
19 | "@tanstack/react-router": "^1.114.17",
20 | "@tauri-apps/api": "^2",
21 | "@tauri-apps/plugin-clipboard-manager": "^2.2.2",
22 | "@tauri-apps/plugin-dialog": "^2.2.0",
23 | "@tauri-apps/plugin-log": "^2.3.1",
24 | "@tauri-apps/plugin-opener": "^2.2.6",
25 | "class-variance-authority": "^0.7.1",
26 | "clsx": "^2.1.1",
27 | "lucide-react": "^0.479.0",
28 | "motion": "^12.5.0",
29 | "neverthrow": "^8.2.0",
30 | "posthog-js": "^1.236.2",
31 | "react": "^19.1.0",
32 | "react-dom": "^19.1.0",
33 | "sonner": "^2.0.1",
34 | "tailwind-merge": "^3.0.2",
35 | "tailwindcss": "^4.1.4",
36 | "tauri-plugin-windows-version-api": "^2.0.0",
37 | "tw-animate-css": "^1.2.5",
38 | "zustand": "^5.0.3"
39 | },
40 | "devDependencies": {
41 | "@tanstack/react-router-devtools": "^1.114.21",
42 | "@tanstack/router-plugin": "^1.114.17",
43 | "@tauri-apps/cli": "^2",
44 | "@types/react": "^19.1.0",
45 | "@types/react-dom": "^19.1.1",
46 | "@vitejs/plugin-react": "^4.3.4",
47 | "prettier": "^3.5.3",
48 | "prettier-plugin-tailwindcss": "^0.6.11",
49 | "typescript": "~5.6.2",
50 | "vite": "^6.0.3"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 | # Generated by Tauri
6 | # will have schema files for capabilities auto-completion
7 | /gen/schemas
8 |
--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "sendit"
3 | version = "0.4.4"
4 | description = "A Tauri App"
5 | authors = ["Sandesh Pandey"]
6 | edition = "2021"
7 |
8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
9 |
10 | [lib]
11 | # The `_lib` suffix may seem redundant but it is necessary
12 | # to make the lib name unique and wouldn't conflict with the bin name.
13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
14 | name = "tauri_send_me_lib"
15 | crate-type = ["staticlib", "cdylib", "rlib"]
16 |
17 | [profile.release]
18 | codegen-units = 1
19 | lto = true
20 |
21 | [build-dependencies]
22 | tauri-build = { version = "2", features = [] }
23 |
24 | [dependencies]
25 | iroh-io = "0.6"
26 | serde = { version = "1", features = ["derive"] }
27 | tokio = { version = "1", features = ["full"] }
28 | tracing = "0.1.40"
29 | n0-future = "0.1.2"
30 |
31 | tauri = { version = "2", features = [] }
32 | tauri-plugin-opener = "2"
33 | serde_json = "1"
34 | tauri-plugin-dialog = "2"
35 | iroh-blobs = { version = "0.34.0", features = ["net_protocol", "rpc"] }
36 | iroh-gossip = { version = "0.34.0", features = ["rpc"] }
37 | iroh = "0.34.0"
38 | quic-rpc = "0.19.0"
39 | tauri-plugin-clipboard-manager = "2"
40 | file_icon_provider = "0.4.0"
41 | image = "0.25.6"
42 | tauri-plugin-log = "2"
43 | log = "0.4.27"
44 | window-vibrancy = "0.6.0"
45 | tauri-plugin-windows-version = "2.0.0"
46 | rand = "0.9.1"
47 | anyhow = "1.0.98"
48 | data-encoding = "2.9.0"
49 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/capabilities/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../gen/schemas/desktop-schema.json",
3 | "identifier": "default",
4 | "description": "Capability for the main window",
5 | "windows": [
6 | "main"
7 | ],
8 | "permissions": [
9 | "core:default",
10 | "dialog:default",
11 | "core:window:allow-start-dragging",
12 | "core:window:allow-minimize",
13 | "core:window:allow-close",
14 | "clipboard-manager:default",
15 | "clipboard-manager:allow-write-text",
16 | "opener:default",
17 | "opener:allow-reveal-item-in-dir",
18 | "log:default",
19 | "windows-version:default"
20 | ]
21 | }
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/64x64.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square107x107Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/Square107x107Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square142x142Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/Square142x142Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/Square150x150Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square284x284Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/Square284x284Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square30x30Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/Square30x30Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/Square310x310Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/Square44x44Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square71x71Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/Square71x71Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square89x89Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/Square89x89Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/StoreLogo.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/ios/AppIcon-20x20@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/ios/AppIcon-20x20@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/ios/AppIcon-20x20@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/ios/AppIcon-20x20@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/ios/AppIcon-29x29@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/ios/AppIcon-29x29@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/ios/AppIcon-29x29@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/ios/AppIcon-29x29@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/ios/AppIcon-40x40@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/ios/AppIcon-40x40@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/ios/AppIcon-40x40@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/ios/AppIcon-40x40@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/ios/AppIcon-512@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/ios/AppIcon-60x60@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/ios/AppIcon-60x60@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/ios/AppIcon-76x76@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/ios/AppIcon-76x76@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frstycodes/sendit/54766504e64fe54c23685f974165921fcf298c9e/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/src-tauri/src/download/mod.rs:
--------------------------------------------------------------------------------
1 | use iroh_gossip::net::GossipReceiver;
2 | use log::{error, info, warn};
3 | use n0_future::stream::StreamExt;
4 | use std::str::FromStr;
5 | use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Instant};
6 | use tauri::{AppHandle, Emitter, Listener, Manager};
7 |
8 | use crate::state::AppState;
9 | use crate::{events, files, state::State, utils};
10 | use iroh::NodeAddr;
11 | use iroh_blobs::{
12 | get::db::DownloadProgress,
13 | store::{ExportFormat, ExportMode},
14 | ticket::BlobTicket,
15 | };
16 |
17 | pub async fn subscribe_upload_progress(rx: GossipReceiver) {
18 | todo!("Need implementation details")
19 | }
20 |
21 | #[tauri::command]
22 | pub async fn download_header(
23 | state: State<'_>,
24 | ticket: String,
25 | handle: AppHandle,
26 | ) -> Result<(), String> {
27 | info!("Downloading with ticket: {}", ticket);
28 | let handle = Arc::new(handle);
29 | let export_dir = Arc::new(utils::get_download_dir(&handle)?);
30 |
31 | let iroh = match cfg!(debug_assertions) {
32 | true => &state.iroh_debug,
33 | false => state.iroh(),
34 | };
35 |
36 | let blobs = &iroh.blobs;
37 |
38 | let ticket =
39 | BlobTicket::from_str(&ticket).map_err(|e| format!("Failed to parse ticket: {}", e))?;
40 | let remote_node_addr = ticket.node_addr().clone();
41 |
42 | // Download and read the header file
43 | let header_content = utils::download_and_read_header(&blobs, ticket).await?;
44 |
45 | let handles = std::sync::Mutex::new(HashMap::new());
46 | let mut files = files::Files::from_str(header_content.as_str())?;
47 | let mut tasks = Vec::with_capacity(files.len());
48 |
49 | for (_, file) in files.drain() {
50 | let payload = events::DownloadFileAdded {
51 | name: file.name.clone(),
52 | icon: file.icon.clone(),
53 | size: file.size.clone(),
54 | };
55 |
56 | handle.emit(events::DOWNLOAD_FILE_ADDED, payload).ok();
57 |
58 | let export_dir = Arc::clone(&export_dir);
59 | let handle = Arc::clone(&handle);
60 | let filename = file.name.clone();
61 | let remote_node_addr = remote_node_addr.clone();
62 |
63 | // Spawn a new task for each file download
64 | let task = tokio::spawn(async move {
65 | let name = file.name.clone();
66 | let res = download_file(&handle, file, &export_dir, remote_node_addr).await;
67 | if let Err(error) = res {
68 | error!("Failed to download file: {}", error);
69 | let payload = events::DownloadFileError { name, error };
70 | handle.emit(events::DOWNLOAD_FILE_ERROR, payload).ok();
71 | };
72 | });
73 |
74 | // Store the abort handler for the task in a map
75 | let handle = task.abort_handle();
76 | handles
77 | .lock()
78 | .map_err(|e| format!("Failed to lock handles: {}", e))?
79 | .insert(filename, handle);
80 |
81 | tasks.push(task);
82 | }
83 |
84 | // Listen for cancel download events
85 | let handle_for_listener = Arc::clone(&handle);
86 | let listener = handle.listen(events::CANCEL_DOWNLOAD, move |event| {
87 | let filename = event.payload().replace("\"", ""); // Remove quotes
88 | if let Ok(mut handles) = handles.lock() {
89 | if let Some(handle) = handles.remove(&filename) {
90 | handle.abort();
91 | let payload = events::DownloadFileAborted {
92 | name: filename.clone(),
93 | reason: "Cancelled by user".to_string(),
94 | };
95 | handle_for_listener
96 | .as_ref()
97 | .emit(events::DOWNLOAD_FILE_ABORTED, payload.clone())
98 | .ok();
99 | info!("Download cancelled for file: {}", filename);
100 | }
101 | }
102 | });
103 |
104 | // Wait for all tasks to complete
105 | for task in tasks {
106 | if let Err(err) = task.await {
107 | error!("Failed to await task: {}", err);
108 | }
109 | }
110 |
111 | // Emit downloads completion event
112 | handle
113 | .as_ref()
114 | .emit(events::DOWNLOAD_ALL_COMPLETE, ())
115 | .map_err(|e| format!("Failed to emit completion event: {}", e))?;
116 |
117 | // Unlisten to the cancel download event
118 | handle.as_ref().unlisten(listener);
119 | Ok(())
120 | }
121 |
122 | async fn download_file(
123 | handle: &AppHandle,
124 | file: files::File,
125 | export_dir: &PathBuf,
126 | remote_node_addr: NodeAddr,
127 | ) -> Result<(), String> {
128 | info!("Started downloading file: {}", file.name);
129 | let state = handle.state::();
130 |
131 | let iroh = match cfg!(debug_assertions) {
132 | true => &state.iroh_debug,
133 | false => state.iroh(),
134 | };
135 |
136 | let blobs = &iroh.blobs;
137 |
138 | let dest = export_dir.join(&file.name);
139 |
140 | // Check if file exists before starting download
141 | if dest.exists() {
142 | let err = format!("File already exists");
143 | handle
144 | .emit(
145 | events::DOWNLOAD_FILE_ERROR,
146 | events::DownloadFileError {
147 | name: file.name.clone(),
148 | error: err.clone(),
149 | },
150 | )
151 | .ok();
152 | return Err(err);
153 | }
154 |
155 | let mut r = blobs
156 | .download(file.hash, remote_node_addr)
157 | .await
158 | .map_err(|e| format!("Failed to download file: {}", e))?;
159 |
160 | let mut last_offset = 0;
161 | let mut timestamp = Instant::now();
162 | let mut size: u64 = 0;
163 | let mut throttle = utils::Throttle::new(std::time::Duration::from_millis(100));
164 |
165 | use DownloadProgress as DP;
166 | while let Some(progress) = r.next().await {
167 | match progress {
168 | Ok(p) => match p {
169 | DP::FoundLocal { size: s, .. } => {
170 | info!("Found Local: {}", file.name);
171 | size = s.value();
172 | }
173 |
174 | DP::Found { size: s, id, .. } => {
175 | info!("Found: {}", id);
176 | size = s;
177 | }
178 |
179 | DP::Progress { offset, .. } if throttle.is_free() => {
180 | let now = Instant::now();
181 | let elapsed = timestamp.elapsed();
182 | let speed = if elapsed.as_micros() > 0 {
183 | (offset - last_offset) as f32 / elapsed.as_micros() as f32
184 | } else {
185 | 0.0
186 | };
187 | timestamp = now;
188 | last_offset = offset;
189 |
190 | if size > 0 {
191 | let percentage = (offset as f32 / size as f32) * 100.0;
192 | let payload = events::DownloadFileProgress {
193 | name: file.name.clone(),
194 | progress: percentage,
195 | speed,
196 | };
197 | handle.emit(events::DOWNLOAD_FILE_PROGRESS, payload).ok();
198 | }
199 | }
200 |
201 | DP::AllDone(..) => {
202 | info!("All Done: {}", file.name);
203 | break;
204 | }
205 |
206 | e => warn!("Unhandled download event: {:?}", e),
207 | },
208 |
209 | Err(e) => {
210 | handle
211 | .emit(
212 | events::DOWNLOAD_FILE_ERROR,
213 | events::DownloadFileError {
214 | name: file.name.clone(),
215 | error: e.to_string(),
216 | },
217 | )
218 | .ok();
219 | return Err(format!("Error during download: {}", e));
220 | }
221 | }
222 | }
223 | // Export the downloaded file
224 | blobs
225 | .export(
226 | file.hash,
227 | dest.clone(),
228 | ExportFormat::Blob,
229 | ExportMode::Copy,
230 | )
231 | .await
232 | .map_err(|e| format!("Error exporting file: {}", e))?
233 | .finish()
234 | .await
235 | .map_err(|e| format!("Error finishing export: {}", e))?;
236 |
237 | info!("Exported file to: {}", file.name);
238 |
239 | // Emit completion event
240 | handle
241 | .emit(
242 | events::DOWNLOAD_FILE_COMPLETED,
243 | events::DownloadFileCompleted {
244 | name: file.name.clone(),
245 | path: dest.display().to_string(),
246 | },
247 | )
248 | .ok();
249 |
250 | Ok(())
251 | }
252 |
--------------------------------------------------------------------------------
/src-tauri/src/events.rs:
--------------------------------------------------------------------------------
1 | use serde::Serialize;
2 |
3 | pub const APP_LOADED: &str = "APP_LOADED";
4 |
5 | // DOWNLOAD
6 | pub const DOWNLOAD_FILE_ADDED: &str = "DOWNLOAD_FILE_ADDED";
7 | pub const DOWNLOAD_FILE_PROGRESS: &str = "DOWNLOAD_FILE_PROGRESS";
8 | pub const DOWNLOAD_FILE_COMPLETED: &str = "DOWNLOAD_FILE_COMPLETED";
9 | pub const DOWNLOAD_ALL_COMPLETE: &str = "DOWNLOAD_ALL_COMPLETE";
10 | pub const DOWNLOAD_FILE_ERROR: &str = "DOWNLOAD_FILE_ERROR";
11 | pub const DOWNLOAD_FILE_ABORTED: &str = "DOWNLOAD_FILE_ABORTED";
12 | pub const CANCEL_DOWNLOAD: &str = "CANCEL_DOWNLOAD";
13 |
14 | #[derive(Debug, Clone, Serialize)]
15 | pub struct DownloadFileAdded {
16 | pub name: String,
17 | pub icon: String,
18 | pub size: u64,
19 | }
20 |
21 | #[derive(Debug, Clone, Serialize)]
22 | pub struct DownloadFileProgress {
23 | pub name: String,
24 | pub progress: f32,
25 | pub speed: f32,
26 | }
27 |
28 | #[derive(Debug, Clone, Serialize)]
29 | pub struct DownloadFileCompleted {
30 | pub name: String,
31 | pub path: String,
32 | }
33 |
34 | #[derive(Debug, Clone, Serialize)]
35 | pub struct DownloadFileAborted {
36 | pub name: String,
37 | pub reason: String,
38 | }
39 |
40 | #[derive(Debug, Clone, Serialize)]
41 | pub struct DownloadFileError {
42 | pub name: String,
43 | pub error: String,
44 | }
45 |
46 | // UPLOAD
47 | pub const UPLOAD_FILE_ADDED: &str = "UPLOAD_FILE_ADDED";
48 | pub const UPLOAD_FILE_PROGRESS: &str = "UPLOAD_FILE_PROGRESS";
49 | pub const UPLOAD_FILE_COMPLETED: &str = "UPLOAD_FILE_COMPLETED";
50 | pub const UPLOAD_FILE_REMOVED: &str = "UPLOAD_FILE_REMOVED";
51 | #[allow(unused)]
52 | pub const UPLOAD_FILE_ERROR: &str = "UPLOAD_FILE_ERROR";
53 |
54 | #[derive(Debug, Clone, Serialize)]
55 | pub struct UploadFileAdded {
56 | pub name: String,
57 | pub icon: String,
58 | pub path: String,
59 | pub size: u64,
60 | }
61 |
62 | #[derive(Debug, Clone, Serialize)]
63 | pub struct UploadFileProgress {
64 | pub path: String,
65 | pub progress: f32,
66 | }
67 |
68 | #[derive(Debug, Clone, Serialize)]
69 | pub struct UploadFileRemoved {
70 | pub name: String,
71 | }
72 |
73 | #[derive(Debug, Clone, Serialize)]
74 | pub struct UploadFileCompleted {
75 | pub name: String,
76 | }
77 |
78 | #[derive(Debug, Clone, Serialize)]
79 | pub struct UploadFileError {
80 | pub name: String,
81 | pub error: String,
82 | }
83 |
84 | // REMOVE_FILE
85 |
--------------------------------------------------------------------------------
/src-tauri/src/file_operations/mod.rs:
--------------------------------------------------------------------------------
1 | use crate::files::{self};
2 | use crate::state::AppState;
3 | use crate::{events, state::State, utils};
4 | use iroh_blobs::rpc::client::blobs::WrapOption;
5 | use iroh_blobs::{provider::AddProgress, util::SetTagOption};
6 | use n0_future::stream::StreamExt;
7 | use serde::Serialize;
8 | use std::path::PathBuf;
9 | use std::sync::Arc;
10 | use std::time::Duration;
11 | use tauri::{AppHandle, Emitter, Manager};
12 | use tokio::time::sleep;
13 | use tracing::{debug, error, info, warn};
14 |
15 | #[derive(Debug, Serialize)]
16 | pub struct ValidatedFile {
17 | name: String,
18 | icon: String,
19 | size: u64,
20 | path: String,
21 | }
22 |
23 | #[tauri::command]
24 | pub async fn validate_files(paths: Vec) -> Result, String> {
25 | let mut files = Vec::new();
26 |
27 | let tasks = paths
28 | .into_iter()
29 | .map(|path| tokio::spawn(async move { validate_file(&path) }));
30 |
31 | for task in tasks {
32 | let task = task.await.map_err(|e| format!("Task error: {}", e))?;
33 | match task {
34 | Ok(file) => files.push(file),
35 | Err(e) => error!(e),
36 | }
37 | }
38 |
39 | Ok(files)
40 | }
41 |
42 | fn validate_file(path: &str) -> Result {
43 | let original_path = path.to_string();
44 |
45 | let path = utils::validate_file_path(path)?;
46 | let icon = utils::get_file_icon(original_path.clone());
47 | let icon = match icon {
48 | Ok(icon) => icon,
49 | Err(e) => {
50 | warn!("{}. Using default value", e);
51 | String::new()
52 | }
53 | };
54 |
55 | let metadata = path
56 | .metadata()
57 | .map_err(|e| format!("Failed to get metadata: {:?}", e))?;
58 |
59 | let size = metadata.len();
60 | let name = utils::file_name_from_path(&path)?;
61 |
62 | let file = ValidatedFile {
63 | name,
64 | icon,
65 | size,
66 | path: original_path,
67 | };
68 |
69 | Ok(file)
70 | }
71 |
72 | #[tauri::command]
73 | pub async fn add_file(state: State<'_>, path: String, handle: AppHandle) -> Result<(), String> {
74 | info!("Adding file: {}", path);
75 |
76 | let original_path = path.clone();
77 | let path = utils::validate_file_path(path)?;
78 | let file_name = utils::file_name_from_path(&path)?;
79 | {
80 | let files = state.files().await;
81 | if files.has_file(&file_name) {
82 | let err = format!("Duplicate file names not allowed.",);
83 | error!("{}", err);
84 | return Err(err);
85 | }
86 | }
87 |
88 | let icon = utils::get_file_icon(original_path.clone());
89 | let icon = match icon {
90 | Ok(i) => i,
91 | Err(e) => {
92 | warn!("{}. Using default value.", e);
93 | String::new()
94 | }
95 | };
96 |
97 | let mut r = state
98 | .iroh()
99 | .blobs
100 | .add_from_path(path.clone(), true, SetTagOption::Auto, WrapOption::NoWrap)
101 | .await
102 | .map_err(|e| format!("Failed to add file: {:?}", e))?;
103 |
104 | let mut size: u64 = 0;
105 | let mut throttle = utils::Throttle::new(Duration::from_millis(32));
106 |
107 | let mut hash = iroh_blobs::Hash::EMPTY;
108 | while let Some(progress) = r.next().await {
109 | match progress {
110 | Ok(p) => match p {
111 | AddProgress::Found {
112 | size: file_size, ..
113 | } => {
114 | info!("Found file: {}", file_name);
115 | size = file_size;
116 |
117 | let payload = events::UploadFileAdded {
118 | name: file_name.clone(),
119 | icon: icon.clone(),
120 | path: original_path.to_string(),
121 | size,
122 | };
123 | handle.emit(events::UPLOAD_FILE_ADDED, payload).ok();
124 | }
125 | AddProgress::Progress { offset, .. } => {
126 | if throttle.is_free() {
127 | let progress_percent = (offset as f32 / size as f32) * 100.0;
128 | debug!("Progress: {}", progress_percent);
129 | let payload = events::UploadFileProgress {
130 | path: file_name.clone(),
131 | progress: progress_percent,
132 | };
133 | handle.emit(events::UPLOAD_FILE_PROGRESS, payload).ok();
134 | }
135 | }
136 | AddProgress::Done { hash: _hash, .. } => {
137 | hash = _hash;
138 | info!("File uploaded: {}", original_path);
139 | let payload = events::UploadFileCompleted {
140 | name: file_name.clone(),
141 | };
142 | handle.emit(events::UPLOAD_FILE_COMPLETED, payload).ok();
143 | }
144 | AddProgress::Abort { .. } => {
145 | info!("Upload aborted: {}", original_path);
146 | }
147 | AddProgress::AllDone { .. } => {
148 | break;
149 | }
150 | },
151 | Err(e) => {
152 | error!("Failed to add file: {:?}", e);
153 | }
154 | }
155 | }
156 |
157 | let file = files::File {
158 | name: file_name,
159 | icon,
160 | size,
161 | hash,
162 | };
163 | let mut files = state.files().await;
164 | files.add_file(file);
165 |
166 | Ok(())
167 | }
168 |
169 |
170 | #[tauri::command]
171 | pub async fn remove_file(path: String, handle: AppHandle) -> Result<(), String> {
172 | let state = handle.state::();
173 |
174 | let path = PathBuf::from(path);
175 | let name = utils::file_name_from_path(&path)?;
176 | info!("Removing file : {}", name);
177 |
178 | let hash = {
179 | let files = state.files().await;
180 | files
181 | .get(&name)
182 | .ok_or_else(|| format!("File not found: {}", name))?
183 | .hash
184 | };
185 |
186 | state
187 | .iroh()
188 | .blobs
189 | .delete_blob(hash)
190 | .await
191 | .map_err(|e| format!("Failed to delete blob: {}", e))?;
192 |
193 | {
194 | let mut files = state.files().await;
195 | files.remove_file(&name);
196 | }
197 |
198 | handle
199 | .emit(
200 | events::UPLOAD_FILE_REMOVED,
201 | events::UploadFileRemoved {
202 | name: name.to_owned(),
203 | },
204 | )
205 | .ok();
206 |
207 | Ok(())
208 | }
209 |
210 | #[tauri::command]
211 | pub async fn remove_all_files(state: State<'_>, handle: AppHandle) -> Result<(), String> {
212 | info!("Removing all files");
213 | let mut files = state.files().await;
214 | let handle = Arc::new(handle);
215 |
216 | let tasks = files
217 | .iter()
218 | .map(|(_, file)| {
219 | let handle = Arc::clone(&handle);
220 | let file = file.clone();
221 | let hash = file.hash;
222 | tokio::spawn(async move {
223 | let state = handle.state::();
224 | state
225 | .iroh()
226 | .blobs
227 | .delete_blob(hash)
228 | .await
229 | .map_err(|e| format!("Failed to delete blob: {}", e))?;
230 |
231 | handle
232 | .emit(
233 | events::UPLOAD_FILE_REMOVED,
234 | events::UploadFileRemoved {
235 | name: file.name.clone(),
236 | },
237 | )
238 | .ok();
239 |
240 | info!("File {} removed successfully", hash);
241 | sleep(Duration::from_secs(2)).await;
242 |
243 | Ok::<(), String>(())
244 | })
245 | })
246 | .collect::>();
247 |
248 | for task in tasks {
249 | let result = task.await.map_err(|e| format!("Task error: {}", e))?;
250 | if let Err(e) = result {
251 | error!("Error removing file: {}", e);
252 | }
253 | }
254 |
255 | // Remove all generated header files
256 | for ticket in state.header_tickets.lock().await.iter() {
257 | state
258 | .iroh()
259 | .blobs
260 | .delete_blob(ticket.hash())
261 | .await
262 | .map_err(|e| format!("Failed to delete blob: {}", e))?;
263 | info!("Ticket {} removed successfully", ticket);
264 | }
265 |
266 | files.clear();
267 | info!("All files removed successfully");
268 | Ok(())
269 | }
270 |
--------------------------------------------------------------------------------
/src-tauri/src/files.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | collections::HashMap,
3 | ops::{Deref, DerefMut},
4 | str::FromStr,
5 | };
6 |
7 | use iroh_blobs::Hash;
8 | use serde::{Deserialize, Serialize};
9 |
10 | use crate::iroh::GossipTicket;
11 |
12 | const VERSION: u32 = 1;
13 |
14 | #[derive(Debug, Clone, Serialize, Deserialize)]
15 | pub struct File {
16 | pub name: String,
17 | pub icon: String,
18 | pub size: u64,
19 | pub hash: Hash,
20 | }
21 |
22 | #[derive(Debug, Clone, Serialize, Deserialize)]
23 | pub struct Files {
24 | pub version: u32,
25 | pub files: HashMap,
26 | gossip_ticket: GossipTicket,
27 | }
28 |
29 | impl Deref for Files {
30 | type Target = HashMap;
31 |
32 | fn deref(&self) -> &Self::Target {
33 | &self.files
34 | }
35 | }
36 |
37 | impl DerefMut for Files {
38 | fn deref_mut(&mut self) -> &mut Self::Target {
39 | &mut self.files
40 | }
41 | }
42 |
43 | impl Files {
44 | pub fn new(ticket: GossipTicket) -> Self {
45 | Self {
46 | version: VERSION,
47 | gossip_ticket: ticket,
48 | files: HashMap::new(),
49 | }
50 | }
51 |
52 | pub fn add_file(&mut self, file: File) {
53 | self.files.insert(file.name.clone(), file);
54 | }
55 |
56 | pub fn remove_file(&mut self, name: &str) {
57 | self.files.remove(name);
58 | }
59 |
60 | pub fn has_file(&self, name: &str) -> bool {
61 | self.files.contains_key(name)
62 | }
63 |
64 | pub fn to_bytes(&self) -> Vec {
65 | serde_json::to_vec(self).expect("Infallible")
66 | }
67 |
68 | pub fn from_bytes(bytes: &[u8]) -> Result {
69 | serde_json::from_slice(bytes).map_err(|e| format!("Failed to parse bytes: {}", e))
70 | }
71 | }
72 |
73 | impl ToString for Files {
74 | fn to_string(&self) -> String {
75 | let text = data_encoding::BASE32.encode(&self.to_bytes());
76 | text
77 | }
78 | }
79 |
80 | impl FromStr for Files {
81 | type Err = String;
82 | fn from_str(s: &str) -> Result {
83 | println!("{s}");
84 | let bytes = data_encoding::BASE32
85 | .decode(s.as_bytes())
86 | .map_err(|e| format!("Failed to decode base32: {}", e))?;
87 |
88 | let files = Self::from_bytes(&bytes)?;
89 |
90 | if files.version != VERSION {
91 | return Err(format!(
92 | "Version mismatch: expected {}, got {}",
93 | VERSION, files.version
94 | ));
95 | }
96 | Ok(files)
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src-tauri/src/iroh.rs:
--------------------------------------------------------------------------------
1 | use std::{ops::Deref, path::PathBuf, str::FromStr};
2 |
3 | use anyhow::Result;
4 | use iroh::{protocol::Router, NodeAddr, NodeId};
5 | use iroh_gossip::{
6 | net::{Gossip, GossipReceiver, GossipSender},
7 | proto::TopicId,
8 | };
9 | use quic_rpc::transport::flume::FlumeConnector;
10 | use serde::{Deserialize, Serialize};
11 |
12 | pub type BlobsClient = iroh_blobs::rpc::client::blobs::Client<
13 | FlumeConnector,
14 | >;
15 |
16 | #[derive(Clone, Debug, Deserialize, Serialize)]
17 | pub struct GossipTicket {
18 | pub topic_id: TopicId,
19 | pub node_id: NodeId,
20 | }
21 |
22 | impl GossipTicket {
23 | pub fn new(topic_id: TopicId, node_id: NodeId) -> Self {
24 | Self { topic_id, node_id }
25 | }
26 | }
27 |
28 | impl GossipTicket {
29 | fn from_bytes(bytes: &[u8]) -> Result {
30 | serde_json::from_slice(bytes).map_err(Into::into)
31 | }
32 |
33 | fn to_bytes(&self) -> Vec {
34 | serde_json::to_vec(self).expect("Infallible")
35 | }
36 | }
37 |
38 | impl FromStr for GossipTicket {
39 | type Err = anyhow::Error;
40 | fn from_str(s: &str) -> Result {
41 | let bytes = data_encoding::BASE32
42 | .decode(s.as_bytes())
43 | .map_err(|_| anyhow::anyhow!("Invalid base32 string"))?;
44 | Self::from_bytes(&bytes)
45 | }
46 | }
47 |
48 | impl ToString for GossipTicket {
49 | fn to_string(&self) -> String {
50 | let mut text = data_encoding::BASE32.encode(&self.to_bytes());
51 | text.make_ascii_lowercase();
52 | text
53 | }
54 | }
55 |
56 | #[derive(Debug)]
57 | pub struct GossipClient {
58 | pub client: Gossip,
59 | ticket: GossipTicket,
60 | channel: GossipChannel,
61 | }
62 |
63 | impl Deref for GossipClient {
64 | type Target = Gossip;
65 |
66 | fn deref(&self) -> &Self::Target {
67 | &self.client
68 | }
69 | }
70 |
71 | impl GossipClient {
72 | pub async fn new(gossip: Gossip, node_id: NodeId) -> Result {
73 | let topic_id = TopicId::from_bytes(rand::random());
74 | let ticket = GossipTicket::new(topic_id, node_id);
75 | let (sender, receiver) = gossip.subscribe(topic_id, vec![])?.split();
76 | let gossip_chan = GossipChannel {
77 | sender,
78 | receiver: Some(receiver),
79 | };
80 |
81 | Ok(Self {
82 | client: gossip,
83 | ticket,
84 | channel: gossip_chan,
85 | })
86 | }
87 |
88 | pub fn channel_mut(&mut self) -> &mut GossipChannel {
89 | &mut self.channel
90 | }
91 |
92 | pub fn ticket(&self) -> &GossipTicket {
93 | &self.ticket
94 | }
95 | }
96 |
97 | #[derive(Debug)]
98 | pub struct GossipChannel {
99 | sender: GossipSender,
100 | receiver: Option,
101 | }
102 |
103 | impl Deref for GossipChannel {
104 | type Target = GossipSender;
105 |
106 | fn deref(&self) -> &Self::Target {
107 | &self.sender
108 | }
109 | }
110 |
111 | impl GossipChannel {
112 | pub fn take_receiver(&mut self) -> Result {
113 | self.receiver
114 | .take()
115 | .ok_or(anyhow::anyhow!("Receiver already taken"))
116 | }
117 | }
118 |
119 | #[derive(Debug)]
120 | pub struct Iroh {
121 | #[allow(dead_code)]
122 | router: Router,
123 | pub blobs: BlobsClient,
124 | pub node_addr: NodeAddr,
125 | pub gossip: GossipClient,
126 | }
127 |
128 | impl Iroh {
129 | pub async fn new(path: PathBuf) -> Result {
130 | // create dir if it doesn't already exist
131 | tokio::fs::create_dir_all(&path).await?;
132 |
133 | // create endpoint
134 | let endpoint = iroh::Endpoint::builder().discovery_n0().bind().await?;
135 |
136 | // build the protocol router
137 | let mut builder = iroh::protocol::Router::builder(endpoint);
138 |
139 | // add iroh blobs
140 | let blobs = iroh_blobs::net_protocol::Blobs::persistent(&path)
141 | .await?
142 | .build(builder.endpoint());
143 | builder = builder.accept(iroh_blobs::ALPN, blobs.clone());
144 |
145 | // add iroh gossip
146 | let gossip = Gossip::builder().spawn(builder.endpoint().clone()).await?;
147 | builder = builder.accept(iroh_gossip::ALPN, gossip.clone());
148 |
149 | let node_addr = builder.endpoint().node_addr().await?;
150 | let router = builder.spawn().await?;
151 | let blobs = blobs.client().clone();
152 | let gossip = GossipClient::new(gossip, node_addr.node_id).await?;
153 |
154 | Ok(Self {
155 | node_addr,
156 | router,
157 | blobs,
158 | gossip,
159 | })
160 | }
161 |
162 | #[allow(dead_code)]
163 | pub async fn shutdown(&self) -> Result<(), String> {
164 | self.router.shutdown().await.map_err(|e| e.to_string())
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/src-tauri/src/lib.rs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
2 |
3 | mod download;
4 | mod events;
5 | mod file_operations;
6 | mod files;
7 | mod iroh;
8 | mod state;
9 | mod theme;
10 | mod ticket;
11 | mod utils;
12 |
13 | use log::LevelFilter;
14 | use state::user_data::{self, User};
15 | use std::{fs, time::Duration};
16 | use tauri::{AppHandle, Emitter, Manager};
17 | use tauri_plugin_log::{Target, TargetKind};
18 | use tokio::time;
19 | use tracing::{error, info};
20 |
21 | const DATA_DIR: &str = ".database";
22 | #[cfg(debug_assertions)]
23 | const DATA_DIR_DEBUG: &str = ".database-test";
24 |
25 | async fn setup(handle: tauri::AppHandle) -> anyhow::Result<()> {
26 | let app_data_dir = match cfg!(debug_assertions) {
27 | true => handle.path().download_dir()?.join(".sendit"),
28 | false => handle.path().app_local_data_dir()?,
29 | };
30 |
31 | let data_dir = app_data_dir.join(DATA_DIR);
32 | fs::create_dir_all(&data_dir)?;
33 | info!("Data directory created at: {}", data_dir.display());
34 |
35 | let mut iroh = iroh::Iroh::new(data_dir).await?;
36 | let channel = &mut iroh.gossip.channel_mut();
37 | #[allow(unused)]
38 | let rx = channel.take_receiver()?;
39 |
40 | #[cfg(debug_assertions)]
41 | let iroh_debug = {
42 | let data_dir = app_data_dir.join(DATA_DIR_DEBUG);
43 | fs::create_dir_all(&data_dir)?;
44 | info!(
45 | "Data directory for debug created at: {}",
46 | data_dir.display()
47 | );
48 |
49 | iroh::Iroh::new(data_dir).await?
50 | };
51 |
52 | let cfg_path = utils::get_config_dir(&handle)
53 | .map_err(|e| anyhow::anyhow!(e))?
54 | .join(user_data::CONFIG_FILE_NAME);
55 |
56 | let user = User::from_config(cfg_path).ok();
57 | handle.manage(state::AppState::new(user, iroh, iroh_debug));
58 |
59 | Ok(())
60 | }
61 |
62 | pub fn notify_app_loaded(handle: AppHandle) {
63 | tokio::spawn(async move {
64 | time::sleep(Duration::from_millis(300)).await;
65 | handle.emit(events::APP_LOADED, ()).ok();
66 | });
67 | }
68 |
69 | fn main() {
70 | tauri::Builder::default()
71 | .plugin(
72 | tauri_plugin_log::Builder::new()
73 | .targets(vec![
74 | Target::new(TargetKind::Stdout),
75 | Target::new(TargetKind::Webview),
76 | ])
77 | .level(LevelFilter::Error)
78 | .level(LevelFilter::Warn)
79 | .level(LevelFilter::Info)
80 | .build(),
81 | )
82 | .plugin(tauri_plugin_windows_version::init()) // WINDOWS VERSION
83 | .plugin(tauri_plugin_clipboard_manager::init()) // CLIPBOARD
84 | .plugin(tauri_plugin_dialog::init()) // DIALOG
85 | .plugin(tauri_plugin_opener::init()) // FILE OPENER
86 | .setup(|app| {
87 | let handle = app.handle().clone();
88 | let window = handle.get_webview_window("main").unwrap();
89 |
90 | #[cfg(debug_assertions)] // Only on Dev environment
91 | window.open_devtools();
92 |
93 | #[cfg(target_os = "windows")]
94 | {
95 | let res = window_vibrancy::apply_mica(&window, None);
96 | if let Err(err) = res {
97 | error!("Error applying mica: {}", err);
98 | }
99 | }
100 |
101 | let handle_clone = handle.clone();
102 | tauri::async_runtime::spawn(async move {
103 | if let Err(err) = setup(handle_clone).await {
104 | error!("Error setting up application: {}", err);
105 | return;
106 | } else {
107 | notify_app_loaded(handle);
108 | }
109 | });
110 |
111 | Ok(())
112 | })
113 | .invoke_handler(tauri::generate_handler![
114 | file_operations::add_file,
115 | file_operations::remove_file,
116 | file_operations::remove_all_files,
117 | file_operations::validate_files,
118 | download::download_header,
119 | ticket::generate_ticket,
120 | theme::set_theme,
121 | state::get_user,
122 | state::update_user,
123 | state::user_data::is_onboarded,
124 | state::app_loaded
125 | ])
126 | .run(tauri::generate_context!())
127 | .expect("Error while running tauri application");
128 | }
129 |
--------------------------------------------------------------------------------
/src-tauri/src/state/mod.rs:
--------------------------------------------------------------------------------
1 | use iroh_blobs::ticket::BlobTicket;
2 | use tauri::{AppHandle, Manager};
3 | use tokio::sync::{Mutex, MutexGuard};
4 | use user_data::User;
5 |
6 | pub mod user_data;
7 |
8 | use crate::files;
9 | use crate::iroh;
10 |
11 | #[derive(Debug)]
12 | pub struct AppState {
13 | #[allow(unused)]
14 | #[cfg(debug_assertions)]
15 | pub iroh_debug: iroh::Iroh,
16 |
17 | pub user: Mutex