mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-24 16:32:45 -04:00
Merge branch 'main' into eng-1896-add-date-range-to-search-options
This commit is contained in:
66
.zed/settings.json
Normal file
66
.zed/settings.json
Normal file
@@ -0,0 +1,66 @@
|
||||
// Folder-specific settings
|
||||
//
|
||||
// For a full list of overridable settings, and general information on folder-specific settings,
|
||||
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
|
||||
{
|
||||
"inlay_hints": {
|
||||
"enabled": false
|
||||
},
|
||||
"languages": {
|
||||
"Rust": {
|
||||
"enable_language_server": true,
|
||||
"formatter": "language_server",
|
||||
"inlay_hints": {
|
||||
"enabled": true,
|
||||
"show_type_hints": true,
|
||||
"show_parameter_hints": true,
|
||||
"show_other_hints": true,
|
||||
"show_background": false,
|
||||
"edit_debounce_ms": 700,
|
||||
"scroll_debounce_ms": 50
|
||||
}
|
||||
},
|
||||
"TOML": {
|
||||
"formatter": "language_server"
|
||||
}
|
||||
},
|
||||
"lsp": {
|
||||
"rust-analyzer": {
|
||||
"initialization_options": {
|
||||
"procMacro": {
|
||||
"enable": true
|
||||
},
|
||||
"diagnostics": {
|
||||
"experimental": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"showUnlinkedFileNotification": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"file_scan_exclusions": [
|
||||
"node_modules",
|
||||
"**/node_modules",
|
||||
"**/bower_components",
|
||||
"**/*.code-search",
|
||||
"**/*.contentlayer",
|
||||
"**/*.next",
|
||||
"**/dist",
|
||||
"apps/mobile/ios/Pods",
|
||||
"apps/mobile/android",
|
||||
"apps/mobile/ios",
|
||||
"**/.git",
|
||||
"**/.svn",
|
||||
"**/.hg",
|
||||
"**/CVS",
|
||||
"**/.DS_Store"
|
||||
],
|
||||
"format_on_save": "on",
|
||||
"ensure_final_newline_on_save": true,
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
"tab_size": 4,
|
||||
"hard_tabs": false,
|
||||
"show_whitespaces": "selection",
|
||||
"show_completion_documentation": true
|
||||
}
|
||||
@@ -38,17 +38,18 @@ This project uses [Cargo](https://doc.rust-lang.org/cargo/getting-started/instal
|
||||
To make changes locally, follow these steps:
|
||||
|
||||
1. Clone & enter the repository:
|
||||
```
|
||||
git clone https://github.com/spacedriveapp/spacedrive && cd spacedrive
|
||||
```
|
||||
Alternatively, if you’ve already cloned the repo locally, pull the latest changes with: `git pull`
|
||||
` git clone https://github.com/spacedriveapp/spacedrive && cd spacedrive
|
||||
`
|
||||
Alternatively, if you’ve already cloned the repo locally, pull the latest changes with: `git pull`
|
||||
> [!TIP]
|
||||
> Consider running `pnpm clean` after pulling the repository if you're returning to it from previously to avoid old files conflicting.
|
||||
2. Configure your system environment for Spacedrive development
|
||||
- For Unix users (Linux / macOS), run: `./scripts/setup.sh`
|
||||
- For Windows users, run: `.\scripts\setup.ps1` via PowerShell.
|
||||
3. Configure your system environment for Spacedrive development
|
||||
|
||||
- For Unix users (Linux / macOS), run: `./scripts/setup.sh`
|
||||
- For Windows users, run: `.\scripts\setup.ps1` via PowerShell.
|
||||
> [!NOTE]
|
||||
> This script ([Unix](https://github.com/spacedriveapp/spacedrive/blob/main/scripts/setup.sh) / [Windows](https://github.com/spacedriveapp/spacedrive/blob/main/scripts/setup.ps1)) will check for if Rust and pnpm are installed then proceed to install any other required dependencies for Spacedrive to build via your system's respective package manager.
|
||||
|
||||
3. Install NodeJS dependencies: `pnpm i`
|
||||
4. Prepare the build: `pnpm prep`. This will run all necessary codegen and build required dependencies.
|
||||
|
||||
@@ -58,13 +59,16 @@ To make changes locally, follow these steps:
|
||||
> The test files will be located in a directory called `test-data` in the root of the Spacedrive repository.
|
||||
|
||||
To run the **desktop** app, run:
|
||||
|
||||
```
|
||||
pnpm tauri dev
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The Tauri desktop app always runs its own instance of the backend and will not connect to a separately initiated `sd-server` instance.
|
||||
|
||||
To run the **backend server**, run:
|
||||
|
||||
```
|
||||
cargo run -p sd-server
|
||||
```
|
||||
@@ -73,23 +77,29 @@ cargo run -p sd-server
|
||||
> If necessary, [DevTools](https://tauri.app/v1/guides/debugging/application/#webview-console) for the WebView can be opened by pressing <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd> (Linux and Windows) or <kbd>Command</kbd>+<kbd>Option</kbd>+<kbd>I</kbd> (macOS) in the desktop app.
|
||||
>
|
||||
> Also, React DevTools can be launched using `pnpx react-devtools`.
|
||||
However, it must be executed before starting the desktop app for it to connect.
|
||||
> However, it must be executed before starting the desktop app for it to connect.
|
||||
|
||||
To run the **web** app (requires the backend to be running), run:
|
||||
|
||||
```
|
||||
pnpm web dev
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> You can also quickly launch the web interface together with the backend with:
|
||||
>
|
||||
> ```
|
||||
> pnpm dev:web
|
||||
> ```
|
||||
|
||||
To run the **e2e tests** for the web app:
|
||||
|
||||
```
|
||||
pnpm web test:e2e
|
||||
```
|
||||
|
||||
If you are developing a new test, you can execute Cypress in interactive mode with:
|
||||
|
||||
```
|
||||
pnpm web test:interactive
|
||||
```
|
||||
@@ -97,19 +107,21 @@ pnpm web test:interactive
|
||||
#### Troubleshooting
|
||||
|
||||
- If you encounter any issues, ensure that you are using the following versions of Rust, Node.js and pnpm:
|
||||
| tool | version |
|
||||
| ---- | ------- |
|
||||
| Rust | [`1.81`](rust-toolchain.toml) |
|
||||
| tool | version |
|
||||
| ---- | ------- |
|
||||
| Rust | [`1.81`](rust-toolchain.toml) |
|
||||
| Node.js | [`18.18`](.nvmrc) |
|
||||
| pnpm | `9.4.0` |
|
||||
| pnpm | `9.4.0` |
|
||||
|
||||
[`rustup`](https://rustup.rs/) & [`nvm`](https://github.com/nvm-sh/nvm) should both pick up on the appropriate versions of the Rust Toolchain & Node respectively from the project automatically.
|
||||
> **Note**: If you get a local migration error in development, you might need to set the following environment variables: [database documentation](docs/developers/architecture/database.mdx#environment-variables).
|
||||
|
||||
- After cleaning out your build artifacts using `pnpm clean`, it's necessary to re-run `pnpm prep`.
|
||||
[`rustup`](https://rustup.rs/) & [`nvm`](https://github.com/nvm-sh/nvm) should both pick up on the appropriate versions of the Rust Toolchain & Node respectively from the project automatically.
|
||||
|
||||
- Make sure to read the [guidelines](https://spacedrive.com/docs/developers/prerequisites/guidelines) to ensure that your code follows a similar style to ours.
|
||||
- After cleaning out your build artifacts using `pnpm clean`, it's necessary to re-run `pnpm prep`.
|
||||
|
||||
- After you finish making your changes and committing them to your branch, make sure to execute `pnpm autoformat` to fix any style inconsistency in your code.
|
||||
- Make sure to read the [guidelines](https://spacedrive.com/docs/developers/prerequisites/guidelines) to ensure that your code follows a similar style to ours.
|
||||
|
||||
- After you finish making your changes and committing them to your branch, make sure to execute `pnpm autoformat` to fix any style inconsistency in your code.
|
||||
|
||||
### Landing Page
|
||||
|
||||
@@ -136,53 +148,56 @@ To run the mobile app:
|
||||
> Most modern phones use `arm64-v8a` while the Android Studio embedded emulator runs `x86_64`
|
||||
|
||||
If you wish to debug directly on a local Android device:
|
||||
- Install [ADB](https://developer.android.com/tools/adb)
|
||||
- On macOS use [homebrew](https://brew.sh/): `brew install adb`
|
||||
- [Configure debugging on your device](https://developer.android.com/tools/adb#Enabling)
|
||||
- Select "Remember this device" & "Trust" when connecting over USB.
|
||||
- Run `pnpm mobile android` with your device connected via USB.
|
||||
|
||||
>[!TIP]
|
||||
- Install [ADB](https://developer.android.com/tools/adb)
|
||||
- On macOS use [homebrew](https://brew.sh/): `brew install adb`
|
||||
- [Configure debugging on your device](https://developer.android.com/tools/adb#Enabling)
|
||||
- Select "Remember this device" & "Trust" when connecting over USB.
|
||||
- Run `pnpm mobile android` with your device connected via USB.
|
||||
|
||||
> [!TIP]
|
||||
> To access the logs from `sd-core` when running on device, run the following command:
|
||||
>
|
||||
> ```
|
||||
> adb logcat | grep -i com.spacedrive.app
|
||||
> ```
|
||||
|
||||
#### iOS
|
||||
- Install the latest version of [Xcode](https://apps.apple.com/au/app/xcode/id497799835) and Simulator if you wish to emulate an iOS device on your Mac.
|
||||
- When running Xcode for the first time, make sure to select the latest version of iOS.
|
||||
- Run `pnpm mobile ios` in the terminal to build & run the app on the Simulator.
|
||||
- To run the app in debug mode with backend (`sd-core`) logging, comment out the following lines before running the above command:
|
||||
https://github.com/spacedriveapp/spacedrive/blob/d180261ca5a93388486742e8f921e895e9ec26a4/apps/mobile/modules/sd-core/ios/build-rust.sh#L51-L54
|
||||
#### iOS
|
||||
|
||||
- Install the latest version of [Xcode](https://apps.apple.com/au/app/xcode/id497799835) and Simulator if you wish to emulate an iOS device on your Mac.
|
||||
- When running Xcode for the first time, make sure to select the latest version of iOS.
|
||||
- Run `pnpm mobile ios` in the terminal to build & run the app on the Simulator.
|
||||
- To run the app in debug mode with backend (`sd-core`) logging, comment out the following lines before running the above command:
|
||||
https://github.com/spacedriveapp/spacedrive/blob/d180261ca5a93388486742e8f921e895e9ec26a4/apps/mobile/modules/sd-core/ios/build-rust.sh#L51-L54
|
||||
You can now get backend (`sd-core`) logs from the Simulator by running the following command:
|
||||
```
|
||||
xcrun simctl launch --console booted com.spacedrive.app
|
||||
```
|
||||
- If you'd like to run the app on device, run:
|
||||
```
|
||||
pnpm mobile ios --device
|
||||
```
|
||||
```
|
||||
xcrun simctl launch --console booted com.spacedrive.app
|
||||
```
|
||||
- If you'd like to run the app on device, run: `pnpm mobile ios --device`
|
||||
> [!IMPORTANT]
|
||||
> Note that you can only get `sd-core` logs from the app when running it on device by running the frontend and backend separately.
|
||||
|
||||
To run the backend (`sd-core`) separately, open up Xcode by running:
|
||||
|
||||
```
|
||||
xed apps/mobile/ios
|
||||
```
|
||||
|
||||
Select from the top if you wish to start on device or Simulator, and press play.
|
||||
|
||||
| Select Device | Run the App | Build & Core logs are found here |
|
||||
| --- | --- | --- |
|
||||
||||
|
||||
| Select Device | Run the App | Build & Core logs are found here |
|
||||
| ---------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- |
|
||||
|  |  |  |
|
||||
|
||||
To run the frontend, run the following:
|
||||
|
||||
```
|
||||
pnpm mobile start
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The frontend is not functional without the sd-core running as well.
|
||||
|
||||
|
||||
### Pull Request
|
||||
|
||||
Once you have finished making your changes, create a pull request (PR) to submit them.
|
||||
|
||||
BIN
Cargo.lock
generated
BIN
Cargo.lock
generated
Binary file not shown.
@@ -20,7 +20,7 @@ rust-version = "1.81"
|
||||
|
||||
[workspace.dependencies]
|
||||
# First party dependencies
|
||||
sd-cloud-schema = { git = "https://github.com/spacedriveapp/cloud-services-schema", rev = "74af2a727c" }
|
||||
sd-cloud-schema = { git = "https://github.com/spacedriveapp/cloud-services-schema", rev = "4e4565bee4" }
|
||||
|
||||
# Third party dependencies used by one or more of our crates
|
||||
async-channel = "2.3"
|
||||
|
||||
@@ -3,16 +3,20 @@ import SwiftRs
|
||||
|
||||
@objc
|
||||
public enum AppThemeType: Int {
|
||||
case auto = -1
|
||||
case light = 0
|
||||
case dark = 1
|
||||
case auto = -1
|
||||
case light = 0
|
||||
case dark = 1
|
||||
}
|
||||
|
||||
var activity: NSObjectProtocol?
|
||||
private let activityLock = NSLock()
|
||||
private var activity: NSObjectProtocol?
|
||||
private var isThemeUpdating = false
|
||||
|
||||
@_cdecl("disable_app_nap")
|
||||
public func disableAppNap(reason: SRString) -> Bool {
|
||||
// Check if App Nap is already disabled
|
||||
activityLock.lock()
|
||||
defer { activityLock.unlock() }
|
||||
|
||||
guard activity == nil else {
|
||||
return false
|
||||
}
|
||||
@@ -26,37 +30,55 @@ public func disableAppNap(reason: SRString) -> Bool {
|
||||
|
||||
@_cdecl("enable_app_nap")
|
||||
public func enableAppNap() -> Bool {
|
||||
// Check if App Nap is already enabled
|
||||
guard let pinfo = activity else {
|
||||
activityLock.lock()
|
||||
defer { activityLock.unlock() }
|
||||
|
||||
guard let currentActivity = activity else {
|
||||
return false
|
||||
}
|
||||
|
||||
ProcessInfo.processInfo.endActivity(pinfo)
|
||||
ProcessInfo.processInfo.endActivity(currentActivity)
|
||||
activity = nil
|
||||
return true
|
||||
}
|
||||
|
||||
@_cdecl("lock_app_theme")
|
||||
public func lockAppTheme(themeType: AppThemeType) {
|
||||
var theme: NSAppearance?
|
||||
switch themeType {
|
||||
case .auto:
|
||||
theme = nil
|
||||
case .dark:
|
||||
theme = NSAppearance(named: .darkAqua)!
|
||||
case .light:
|
||||
theme = NSAppearance(named: .aqua)!
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NSApp.appearance = theme
|
||||
|
||||
// Trigger a repaint of the window
|
||||
if let window = NSApplication.shared.mainWindow {
|
||||
window.invalidateShadow()
|
||||
window.displayIfNeeded()
|
||||
// Prevent concurrent theme updates
|
||||
guard !isThemeUpdating else {
|
||||
return
|
||||
}
|
||||
|
||||
isThemeUpdating = true
|
||||
|
||||
let theme: NSAppearance?
|
||||
switch themeType {
|
||||
case .auto:
|
||||
theme = nil
|
||||
case .dark:
|
||||
theme = NSAppearance(named: .darkAqua)
|
||||
case .light:
|
||||
theme = NSAppearance(named: .aqua)
|
||||
}
|
||||
|
||||
// Use sync to ensure completion before return
|
||||
DispatchQueue.main.sync {
|
||||
autoreleasepool {
|
||||
NSApp.appearance = theme
|
||||
|
||||
if let window = NSApplication.shared.mainWindow {
|
||||
NSAnimationContext.runAnimationGroup({ context in
|
||||
context.duration = 0
|
||||
window.invalidateShadow()
|
||||
window.displayIfNeeded()
|
||||
}, completionHandler: {
|
||||
isThemeUpdating = false
|
||||
})
|
||||
} else {
|
||||
isThemeUpdating = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("set_titlebar_style")
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
"lint": "eslint src --cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"@spacedrive/rspc-client": "github:spacedriveapp/rspc#path:packages/client&6a77167495",
|
||||
"@spacedrive/rspc-tauri": "github:spacedriveapp/rspc#path:packages/tauri&6a77167495",
|
||||
"@crabnebula/tauri-plugin-drag": "^2.0.0",
|
||||
"@remix-run/router": "=1.13.1",
|
||||
"@sd/client": "workspace:*",
|
||||
"@sd/interface": "workspace:*",
|
||||
"@sd/ui": "workspace:*",
|
||||
"@spacedrive/rspc-client": "github:spacedriveapp/rspc#path:packages/client&6a77167495",
|
||||
"@spacedrive/rspc-tauri": "github:spacedriveapp/rspc#path:packages/tauri&6a77167495",
|
||||
"@t3-oss/env-core": "^0.7.1",
|
||||
"@tanstack/react-query": "^5.59",
|
||||
"@tauri-apps/api": "=2.0.3",
|
||||
@@ -31,7 +32,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "=6.20.1",
|
||||
"sonner": "^1.0.3",
|
||||
"supertokens-web-js": "^0.13.0"
|
||||
"supertokens-web-js": "=0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sd/config": "workspace:*",
|
||||
|
||||
@@ -32,22 +32,25 @@ thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
tracing = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde"] }
|
||||
base64 = { workspace = true }
|
||||
|
||||
# Specific Desktop dependencies
|
||||
# WARNING: Do NOT enable default features, as that vendors dbus (see below)
|
||||
opener = { version = "0.7.1", features = ["reveal"], default-features = false }
|
||||
specta-typescript = "=0.0.7"
|
||||
tauri-plugin-clipboard-manager = "=2.0.1"
|
||||
tauri-plugin-cors-fetch = { path = "../../../crates/tauri-plugin-cors-fetch" }
|
||||
tauri-plugin-deep-link = "=2.0.1"
|
||||
tauri-plugin-dialog = "=2.0.3"
|
||||
tauri-plugin-http = "=2.0.3"
|
||||
tauri-plugin-os = "=2.0.1"
|
||||
tauri-plugin-shell = "=2.0.2"
|
||||
tauri-plugin-updater = "=2.0.2"
|
||||
tauri-plugin-drag = "2.0.0"
|
||||
drag = "2.0.0"
|
||||
|
||||
# memory allocator
|
||||
mimalloc = { workspace = true }
|
||||
tauri-plugin-cors-fetch = "2.1.1"
|
||||
mimalloc = { workspace = true }
|
||||
|
||||
[dependencies.tauri]
|
||||
features = ["linux-libxdo", "macos-private-api", "native-tls-vendored", "unstable"]
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"core:window:allow-start-dragging",
|
||||
"core:webview:allow-internal-toggle-devtools",
|
||||
"cors-fetch:default",
|
||||
"drag:default",
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
|
||||
223
apps/desktop/src-tauri/src/drag.rs
Normal file
223
apps/desktop/src-tauri/src/drag.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
// Import required dependencies for drag and drop operations, serialization, and async functionality
|
||||
use drag::{DragItem, Image, Options};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
use tauri::{ipc::Channel, Manager, PhysicalPosition, State, WebviewWindow};
|
||||
|
||||
// DragState wraps a thread-safe boolean flag to track drag operation status
|
||||
#[derive(Clone)]
|
||||
pub struct DragState(pub Arc<Mutex<bool>>);
|
||||
|
||||
// Default implementation for DragState initializes with false
|
||||
impl Default for DragState {
|
||||
fn default() -> Self {
|
||||
Self(Arc::new(Mutex::new(false)))
|
||||
}
|
||||
}
|
||||
|
||||
// Enum to represent the result of a drag operation (serializable for IPC)
|
||||
#[derive(Serialize, Deserialize, Type, Clone)]
|
||||
pub enum WrappedDragResult {
|
||||
Dropped,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
// Structure to hold cursor position coordinates (serializable for IPC)
|
||||
#[derive(Serialize, Deserialize, Type, Clone)]
|
||||
pub struct WrappedCursorPosition {
|
||||
x: i32,
|
||||
y: i32,
|
||||
}
|
||||
|
||||
// Combined structure for drag operation results (serializable for IPC)
|
||||
#[derive(Serialize, Deserialize, Type, Clone)]
|
||||
pub struct CallbackResult {
|
||||
result: WrappedDragResult,
|
||||
#[serde(rename = "cursorPos")]
|
||||
cursor_pos: WrappedCursorPosition,
|
||||
}
|
||||
|
||||
// Conversion implementations for drag-rs types to our wrapped types
|
||||
impl From<drag::DragResult> for WrappedDragResult {
|
||||
fn from(result: drag::DragResult) -> Self {
|
||||
match result {
|
||||
drag::DragResult::Dropped => WrappedDragResult::Dropped,
|
||||
drag::DragResult::Cancel => WrappedDragResult::Cancel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<drag::CursorPosition> for WrappedCursorPosition {
|
||||
fn from(pos: drag::CursorPosition) -> Self {
|
||||
WrappedCursorPosition { x: pos.x, y: pos.y }
|
||||
}
|
||||
}
|
||||
|
||||
// Global flag to track if position tracking is active
|
||||
static TRACKING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[tauri::command(async)]
|
||||
/// Initiates a drag and drop operation with cursor position tracking
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `window` - The Tauri window instance
|
||||
/// * `_state` - Current drag state (unused)
|
||||
/// * `files` - Vector of file paths to be dragged
|
||||
/// * `icon_path` - Path to the preview icon for the drag operation
|
||||
/// * `on_event` - Channel for communicating drag operation events back to the frontend
|
||||
#[specta::specta]
|
||||
pub async fn start_drag(
|
||||
window: WebviewWindow,
|
||||
_state: State<'_, DragState>,
|
||||
files: Vec<String>,
|
||||
icon_path: String,
|
||||
on_event: Channel<CallbackResult>,
|
||||
) -> Result<(), String> {
|
||||
// Fast atomic swap for tracking state
|
||||
match TRACKING.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) {
|
||||
Ok(_) => {
|
||||
println!("Starting position tracking");
|
||||
}
|
||||
Err(_) => {
|
||||
// If already tracking, stop previous instance quickly
|
||||
TRACKING.store(false, Ordering::SeqCst);
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(16)).await;
|
||||
TRACKING.store(true, Ordering::SeqCst);
|
||||
println!("Restarting position tracking");
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-allocate resources before spawning task
|
||||
let window_handle = Arc::new(window);
|
||||
let app_handle = window_handle.app_handle();
|
||||
|
||||
// Initialize control flags
|
||||
let cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
let is_completed = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// Prepare resources once with minimal cloning
|
||||
let tracking_resources = Arc::new((files.clone(), icon_path.clone(), Arc::new(on_event)));
|
||||
|
||||
println!("Starting position tracking");
|
||||
|
||||
// Get handles for window and app management
|
||||
let window_clone = window_handle.clone();
|
||||
let app_handle_owned = app_handle.to_owned();
|
||||
let window_owned = window_clone.to_owned();
|
||||
|
||||
// Control flags for operation state
|
||||
let is_completed_clone = is_completed.clone();
|
||||
|
||||
// Spawn background task for cursor tracking
|
||||
tokio::spawn(async move {
|
||||
// Initialize tracking state
|
||||
let mut last_position = (0.0, 0.0);
|
||||
let mut last_message_time = Instant::now();
|
||||
let threshold = 1.0; // Minimum movement threshold
|
||||
let message_debounce = Duration::from_millis(32); // State update interval
|
||||
let mut was_inside = false;
|
||||
|
||||
// Main tracking loop
|
||||
while TRACKING.load(Ordering::SeqCst) && !is_completed.load(Ordering::SeqCst) {
|
||||
let window_for_check = window_owned.clone();
|
||||
// Skip if window is not focused
|
||||
if !window_for_check.is_focused().unwrap_or(false) {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(8)).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get current cursor and window positions
|
||||
if let (Ok(cursor_position), Ok(window_position), Ok(window_size)) = (
|
||||
window_for_check.cursor_position(),
|
||||
window_for_check.outer_position(),
|
||||
window_for_check.inner_size(),
|
||||
) {
|
||||
// Calculate cursor position relative to window
|
||||
let relative_position = PhysicalPosition::new(
|
||||
cursor_position.x - window_position.x as f64,
|
||||
cursor_position.y - window_position.y as f64,
|
||||
);
|
||||
|
||||
// Check if cursor is inside window boundaries
|
||||
let is_inside = relative_position.x >= 0.0
|
||||
&& relative_position.y >= 0.0
|
||||
&& relative_position.x <= window_size.width as f64
|
||||
&& relative_position.y <= window_size.height as f64;
|
||||
|
||||
// Process state changes if cursor moved enough
|
||||
if is_inside != was_inside
|
||||
&& ((relative_position.x - last_position.0).abs() > threshold
|
||||
|| (relative_position.y - last_position.1).abs() > threshold)
|
||||
{
|
||||
let now = Instant::now();
|
||||
if now.duration_since(last_message_time) >= message_debounce {
|
||||
// Prepare resources for drag operation
|
||||
let files_for_drag = tracking_resources.0.clone();
|
||||
let icon_path_for_drag = tracking_resources.1.clone();
|
||||
let on_event_for_drag = tracking_resources.2.clone();
|
||||
let is_completed = is_completed_clone.clone();
|
||||
let cancel_flag_clone = cancel_flag.clone();
|
||||
let window_for_drag = window_owned.clone();
|
||||
|
||||
// Execute drag operation on main thread
|
||||
app_handle_owned
|
||||
.run_on_main_thread(move || {
|
||||
if !is_inside {
|
||||
println!("Starting drag operation");
|
||||
// Create drag items
|
||||
let paths: Vec<PathBuf> =
|
||||
files_for_drag.iter().map(PathBuf::from).collect();
|
||||
let item = DragItem::Files(paths);
|
||||
let preview_icon =
|
||||
Image::File(PathBuf::from(&icon_path_for_drag));
|
||||
|
||||
// Start the drag operation
|
||||
if let Ok(_) = drag::start_drag(
|
||||
&window_for_drag,
|
||||
item,
|
||||
preview_icon,
|
||||
move |result, cursor_pos| {
|
||||
// Send result back to frontend
|
||||
let _ = on_event_for_drag.send(CallbackResult {
|
||||
result: result.into(),
|
||||
cursor_pos: cursor_pos.into(),
|
||||
});
|
||||
// Mark operation as completed
|
||||
is_completed.store(true, Ordering::SeqCst);
|
||||
TRACKING.store(false, Ordering::SeqCst);
|
||||
},
|
||||
Options::default(),
|
||||
) {
|
||||
println!("Drag operation started");
|
||||
}
|
||||
} else {
|
||||
println!("Cursor returned to window");
|
||||
cancel_flag_clone.store(true, Ordering::SeqCst);
|
||||
// We have this for now, but technically, it doesn't do anything.
|
||||
// I'm still trying to figure out how to cancel mid-drag without the user having to cancel the dragging on the frontend too.
|
||||
// - @Rocky43007
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Update tracking state
|
||||
last_message_time = now;
|
||||
was_inside = is_inside;
|
||||
last_position = (relative_position.x, relative_position.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent excessive CPU usage
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(8)).await;
|
||||
}
|
||||
|
||||
println!("Tracking instance stopped");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -22,6 +22,7 @@ use tokio::task::block_in_place;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, error};
|
||||
|
||||
mod drag;
|
||||
mod file;
|
||||
mod menu;
|
||||
mod tauri_plugins;
|
||||
@@ -200,6 +201,7 @@ async fn main() -> tauri::Result<()> {
|
||||
set_menu_bar_item_state,
|
||||
request_fda_macos,
|
||||
open_trash_in_os_explorer,
|
||||
drag::start_drag,
|
||||
file::open_file_paths,
|
||||
file::open_ephemeral_files,
|
||||
file::get_file_path_open_with_apps,
|
||||
@@ -362,6 +364,7 @@ async fn main() -> tauri::Result<()> {
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(updater::plugin())
|
||||
.manage(updater::State::default())
|
||||
.manage(drag::DragState::default())
|
||||
.build(tauri::generate_context!())?
|
||||
.run(|_, _| {});
|
||||
|
||||
|
||||
@@ -3,7 +3,13 @@ import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { PropsWithChildren, startTransition, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { RspcProvider, useBridgeMutation } from '@sd/client';
|
||||
import {
|
||||
getItemFilePath,
|
||||
libraryClient,
|
||||
RspcProvider,
|
||||
useBridgeMutation,
|
||||
useSelector
|
||||
} from '@sd/client';
|
||||
import {
|
||||
createRoutes,
|
||||
DeeplinkEvent,
|
||||
@@ -16,13 +22,15 @@ import {
|
||||
} from '@sd/interface';
|
||||
import { RouteTitleContext } from '@sd/interface/hooks/useRouteTitle';
|
||||
|
||||
import '@sd/ui/style/style.scss';
|
||||
import '@sd/ui/style';
|
||||
|
||||
import { Channel, invoke } from '@tauri-apps/api/core';
|
||||
import SuperTokens from 'supertokens-web-js';
|
||||
import EmailPassword from 'supertokens-web-js/recipe/emailpassword';
|
||||
import Passwordless from 'supertokens-web-js/recipe/passwordless';
|
||||
import Session from 'supertokens-web-js/recipe/session';
|
||||
import ThirdParty from 'supertokens-web-js/recipe/thirdparty';
|
||||
import { explorerStore } from '@sd/interface/app/$libraryId/Explorer/store';
|
||||
// TODO: Bring this back once upstream is fixed up.
|
||||
// const client = hooks.createClient({
|
||||
// links: [
|
||||
@@ -37,6 +45,7 @@ import getWindowHandler from '@sd/interface/app/$libraryId/settings/client/accou
|
||||
import { useLocale } from '@sd/interface/hooks';
|
||||
import { AUTH_SERVER_URL, getTokens } from '@sd/interface/util';
|
||||
|
||||
import { Transparent } from '../../../packages/assets/images';
|
||||
import { commands } from './commands';
|
||||
import { platform } from './platform';
|
||||
import { queryClient } from './query';
|
||||
@@ -46,6 +55,7 @@ import { createUpdater } from './updater';
|
||||
declare global {
|
||||
interface Window {
|
||||
enableCORSFetch: (enable: boolean) => void;
|
||||
useDragAndDrop: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,11 +77,89 @@ SuperTokens.init({
|
||||
|
||||
const startupError = (window as any).__SD_ERROR__ as string | undefined;
|
||||
|
||||
function useDragAndDrop() {
|
||||
const dragState = useSelector(explorerStore, (s) => s.drag);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Drag effect triggered:', {
|
||||
dragStateType: dragState?.type,
|
||||
itemCount: dragState?.type === 'dragging' ? dragState?.items?.length : undefined
|
||||
});
|
||||
|
||||
(async () => {
|
||||
if (dragState?.type === 'dragging' && dragState.items.length > 0) {
|
||||
console.log('Starting drag operation with items:', dragState.items);
|
||||
|
||||
const items = await Promise.all(
|
||||
dragState.items.map(async (item) => {
|
||||
const data = getItemFilePath(item);
|
||||
if (!data) {
|
||||
console.log('No file path data for item:', item);
|
||||
return;
|
||||
}
|
||||
|
||||
const file_path =
|
||||
'path' in data ? data.path : await libraryClient.query(['files.getPath', data.id]);
|
||||
|
||||
console.log('Resolved file path:', file_path);
|
||||
return {
|
||||
type: 'explorer-item',
|
||||
file_path: file_path
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const image = Transparent.split('/@fs')[1]!;
|
||||
console.log('Using preview image:', image);
|
||||
|
||||
const validFiles = items.filter(Boolean).map((item) => item?.file_path);
|
||||
console.log('Invoking start_drag with files:', validFiles);
|
||||
|
||||
try {
|
||||
const channel = new Channel<{
|
||||
result: 'Dropped' | 'Cancelled';
|
||||
cursorPos: { x: number; y: number };
|
||||
}>();
|
||||
|
||||
channel.onmessage = (payload) => {
|
||||
console.log('Drag completed:', {
|
||||
result: payload.result,
|
||||
position: payload.cursorPos,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (payload.result === 'Dropped') {
|
||||
console.log('Drop location:', {
|
||||
x: payload.cursorPos.x,
|
||||
y: payload.cursorPos.y,
|
||||
screen: window.screen
|
||||
});
|
||||
}
|
||||
|
||||
explorerStore.drag = null;
|
||||
};
|
||||
|
||||
await invoke('start_drag', {
|
||||
files: validFiles,
|
||||
iconPath: image,
|
||||
onEvent: channel
|
||||
});
|
||||
console.log('start_drag invoked successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to start drag:', error);
|
||||
explorerStore.drag = null;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [dragState]);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
useEffect(() => {
|
||||
// This tells Tauri to show the current window because it's finished loading
|
||||
commands.appReady();
|
||||
window.enableCORSFetch(true);
|
||||
window.useDragAndDrop = useDragAndDrop;
|
||||
// .then(() => {
|
||||
// if (import.meta.env.PROD) window.fetch = fetch;
|
||||
// });
|
||||
@@ -126,13 +214,15 @@ type RedirectPath = { pathname: string; search: string | undefined };
|
||||
function AppInner() {
|
||||
const [tabs, setTabs] = useState(() => [createTab()]);
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||
const tokens = getTokens();
|
||||
const cloudBootstrap = useBridgeMutation('cloud.bootstrap');
|
||||
|
||||
useEffect(() => {
|
||||
// If the access token and/or refresh token are missing, we need to skip the cloud bootstrap
|
||||
if (tokens.accessToken.length === 0 || tokens.refreshToken.length === 0) return;
|
||||
cloudBootstrap.mutate([tokens.accessToken, tokens.refreshToken]);
|
||||
(async () => {
|
||||
const tokens = await getTokens();
|
||||
// If the access token and/or refresh token are missing, we need to skip the cloud bootstrap
|
||||
if (tokens.accessToken.length === 0 || tokens.refreshToken.length === 0) return;
|
||||
cloudBootstrap.mutate([tokens.accessToken, tokens.refreshToken]);
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -204,6 +294,43 @@ function AppInner() {
|
||||
};
|
||||
}, [selectedTab.element]);
|
||||
|
||||
const SizeDisplay = () => {
|
||||
const [size, setSize] = useState({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: 10,
|
||||
right: 10,
|
||||
background: 'rgba(0,0,0,0.7)',
|
||||
color: 'white',
|
||||
padding: '5px 10px',
|
||||
borderRadius: '5px',
|
||||
fontSize: '12px',
|
||||
zIndex: 9999
|
||||
}}
|
||||
>
|
||||
{size.width} x {size.height}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<RouteTitleContext.Provider
|
||||
value={useMemo(
|
||||
@@ -252,8 +379,7 @@ function AppInner() {
|
||||
new Promise((res) => {
|
||||
startTransition(() => {
|
||||
setTabs((tabs) => {
|
||||
const { pathname, search } =
|
||||
selectedTab.router.state.location;
|
||||
const { pathname, search } = selectedTab.router.state.location;
|
||||
const newTab = createTab({ pathname, search });
|
||||
const newTabs = [...tabs, newTab];
|
||||
|
||||
@@ -303,6 +429,7 @@ function AppInner() {
|
||||
tab.element
|
||||
)
|
||||
)}
|
||||
{/* <SizeDisplay /> */}
|
||||
<div ref={ref} />
|
||||
</SpacedriveInterfaceRoot>
|
||||
</PlatformUpdaterProvider>
|
||||
|
||||
28
apps/desktop/src/commands.ts
generated
28
apps/desktop/src/commands.ts
generated
@@ -49,6 +49,31 @@ export const commands = {
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Initiates a drag and drop operation with cursor position tracking
|
||||
*
|
||||
* # Arguments
|
||||
* * `window` - The Tauri window instance
|
||||
* * `_state` - Current drag state (unused)
|
||||
* * `files` - Vector of file paths to be dragged
|
||||
* * `icon_path` - Path to the preview icon for the drag operation
|
||||
* * `on_event` - Channel for communicating drag operation events back to the frontend
|
||||
*/
|
||||
async startDrag(
|
||||
files: string[],
|
||||
iconPath: string,
|
||||
onEvent: TAURI_CHANNEL<CallbackResult>
|
||||
): Promise<Result<null, string>> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('start_drag', { files, iconPath, onEvent })
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async openFilePaths(
|
||||
library: string,
|
||||
ids: number[]
|
||||
@@ -162,6 +187,7 @@ export const events = __makeEvents__<{
|
||||
/** user-defined types **/
|
||||
|
||||
export type AppThemeType = 'Auto' | 'Light' | 'Dark';
|
||||
export type CallbackResult = { result: WrappedDragResult; cursorPos: WrappedCursorPosition };
|
||||
export type DragAndDropEvent =
|
||||
| { type: 'Hovered'; paths: string[]; x: number; y: number }
|
||||
| { type: 'Dropped'; paths: string[]; x: number; y: number }
|
||||
@@ -199,6 +225,8 @@ export type RevealItem =
|
||||
| { FilePath: { id: number } }
|
||||
| { Ephemeral: { path: string } };
|
||||
export type Update = { version: string };
|
||||
export type WrappedCursorPosition = { x: number; y: number };
|
||||
export type WrappedDragResult = 'Dropped' | 'Cancel';
|
||||
|
||||
type __EventObj__<T> = {
|
||||
listen: (cb: TAURI_API_EVENT.EventCallback<T>) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Preview } from '@storybook/react';
|
||||
|
||||
import '@sd/ui/style/style.scss';
|
||||
import '@sd/ui/style';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
|
||||
@@ -39,15 +39,15 @@ zeroize = { workspace = true }
|
||||
# External dependencies
|
||||
anyhow = "1.0.86"
|
||||
dashmap = "6.1.0"
|
||||
iroh-net = { version = "0.28.1", features = ["discovery-local-network", "iroh-relay"] }
|
||||
iroh = { version = "0.29.0", features = ["discovery-local-network"] }
|
||||
paste = "=1.0.15"
|
||||
quic-rpc = { version = "0.15.1", features = ["iroh-net-transport", "quinn-transport"] }
|
||||
quic-rpc = { version = "0.17.1", features = ["iroh-transport", "quinn-transport"] }
|
||||
quinn = { package = "iroh-quinn", version = "0.12" }
|
||||
# Using whatever version of reqwest that reqwest-middleware uses, just putting here to enable some features
|
||||
reqwest = { version = "0.12", features = ["json", "native-tls-vendored", "stream"] }
|
||||
reqwest-middleware = { version = "0.4", features = ["json"] }
|
||||
reqwest-retry = "0.7"
|
||||
rustls = { version = "=0.23.16", default-features = false, features = ["brotli", "ring", "std"] }
|
||||
rustls = { version = "=0.23.19", default-features = false, features = ["brotli", "ring", "std"] }
|
||||
rustls-platform-verifier = "0.4.0"
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use sd_cloud_schema::{Client, Request, Response, ServicesALPN};
|
||||
use std::{net::SocketAddr, sync::Arc, time::Duration};
|
||||
|
||||
use futures::Stream;
|
||||
use iroh_net::relay::RelayUrl;
|
||||
use iroh::relay::RelayUrl;
|
||||
use quic_rpc::{transport::quinn::QuinnConnector, RpcClient, RpcMessage};
|
||||
use quinn::{crypto::rustls::QuicClientConfig, ClientConfig, Endpoint};
|
||||
use reqwest::{IntoUrl, Url};
|
||||
|
||||
@@ -11,13 +11,12 @@ use sd_crypto::{CryptoRng, SeedableRng};
|
||||
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use iroh_net::{
|
||||
use iroh::{
|
||||
discovery::{
|
||||
dns::DnsDiscovery, local_swarm_discovery::LocalSwarmDiscovery, pkarr::dht::DhtDiscovery,
|
||||
ConcurrentDiscovery, Discovery,
|
||||
},
|
||||
relay::{RelayMap, RelayMode, RelayUrl},
|
||||
Endpoint, NodeId,
|
||||
Endpoint, NodeId, RelayMap, RelayMode, RelayUrl,
|
||||
};
|
||||
use reqwest::Url;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -9,7 +9,7 @@ use sd_cloud_schema::{
|
||||
use std::time::Duration;
|
||||
|
||||
use futures_concurrency::future::Join;
|
||||
use iroh_net::{Endpoint, NodeId};
|
||||
use iroh::{Endpoint, NodeId};
|
||||
use quic_rpc::{transport::quinn::QuinnConnector, RpcClient};
|
||||
use tokio::time::Instant;
|
||||
use tracing::{debug, error, instrument, warn};
|
||||
|
||||
@@ -27,7 +27,7 @@ use dashmap::DashMap;
|
||||
use flume::SendError;
|
||||
use futures::StreamExt;
|
||||
use futures_concurrency::stream::Merge;
|
||||
use iroh_net::{Endpoint, NodeId};
|
||||
use iroh::{Endpoint, NodeId};
|
||||
use quic_rpc::{
|
||||
server::{Accepting, RpcChannel, RpcServerError},
|
||||
transport::quinn::{QuinnConnector, QuinnListener},
|
||||
|
||||
346
core/src/api/keys.rs
Normal file
346
core/src/api/keys.rs
Normal file
@@ -0,0 +1,346 @@
|
||||
use super::utils::library;
|
||||
use super::{Ctx, SanitizedNodeConfig, R};
|
||||
use rspc::{alpha::AlphaRouter, ErrorCode};
|
||||
use sd_crypto::cookie::CookieCipher;
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CipherCache {
|
||||
uuid: String,
|
||||
cipher: CookieCipher,
|
||||
}
|
||||
|
||||
async fn get_cipher(
|
||||
node: &Ctx,
|
||||
cache: Arc<RwLock<Option<CipherCache>>>,
|
||||
) -> Result<CookieCipher, rspc::Error> {
|
||||
let config = SanitizedNodeConfig::from(node.config.get().await);
|
||||
let uuid = config.id.to_string();
|
||||
|
||||
{
|
||||
let cache_read = cache.read().await;
|
||||
if let Some(ref cache) = *cache_read {
|
||||
if cache.uuid == uuid {
|
||||
return Ok(cache.cipher.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let uuid_key = CookieCipher::generate_key_from_string(&uuid).map_err(|e| {
|
||||
error!("Failed to generate key: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to generate key".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let cipher = CookieCipher::new(&uuid_key).map_err(|e| {
|
||||
error!("Failed to create cipher: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to create cipher".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
{
|
||||
let mut cache_write = cache.write().await;
|
||||
*cache_write = Some(CipherCache {
|
||||
uuid,
|
||||
cipher: cipher.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(cipher)
|
||||
}
|
||||
|
||||
async fn read_file(path: &Path) -> Result<Vec<u8>, rspc::Error> {
|
||||
tokio::fs::read(path).await.map_err(|e| {
|
||||
error!("Failed to read file: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
format!("Failed to read file {:?}", path),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn write_file(path: &Path, data: &[u8]) -> Result<(), rspc::Error> {
|
||||
let mut file = tokio::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(path)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to open file: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
format!("Failed to open file {:?}", path),
|
||||
)
|
||||
})?;
|
||||
file.write_all(data).await.map_err(|e| {
|
||||
error!("Failed to write to file: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
format!("Failed to write to file {:?}", path),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn sanitize_path(base_dir: &Path, path: &Path) -> Result<PathBuf, rspc::Error> {
|
||||
let abs_base = base_dir.canonicalize().map_err(|e| {
|
||||
error!("Failed to canonicalize base directory: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to canonicalize base directory".to_string(),
|
||||
)
|
||||
})?;
|
||||
let abs_path = abs_base.join(path).canonicalize().map_err(|e| {
|
||||
error!("Failed to canonicalize path: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to canonicalize path".to_string(),
|
||||
)
|
||||
})?;
|
||||
if abs_path.starts_with(&abs_base) {
|
||||
Ok(abs_path)
|
||||
} else {
|
||||
error!("Path injection attempt detected: {:?}", abs_path);
|
||||
Err(rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Invalid path".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
let cipher_cache = Arc::new(RwLock::new(None));
|
||||
|
||||
R.router()
|
||||
.procedure("get", {
|
||||
let cipher_cache = cipher_cache.clone();
|
||||
R.query(move |node, _: ()| {
|
||||
let cipher_cache = cipher_cache.clone();
|
||||
async move {
|
||||
let base_dir = node.config.data_directory();
|
||||
let path = sanitize_path(&base_dir, Path::new(".sdks"))?;
|
||||
let data = read_file(&path).await?;
|
||||
let cipher = get_cipher(&node, cipher_cache).await?;
|
||||
|
||||
let data_str = String::from_utf8(data).map_err(|e| {
|
||||
error!("Failed to convert data to string: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to convert data to string".to_string(),
|
||||
)
|
||||
})?;
|
||||
let data = CookieCipher::base64_decode(&data_str).map_err(|e| {
|
||||
error!("Failed to decode data: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to decode data".to_string(),
|
||||
)
|
||||
})?;
|
||||
let de_data = cipher.decrypt(&data).map_err(|e| {
|
||||
error!("Failed to decrypt data: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to decrypt data".to_string(),
|
||||
)
|
||||
})?;
|
||||
let de_data = String::from_utf8(de_data).map_err(|e| {
|
||||
error!("Failed to convert data to string: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to convert data to string".to_string(),
|
||||
)
|
||||
})?;
|
||||
Ok(de_data)
|
||||
}
|
||||
})
|
||||
})
|
||||
.procedure("save", {
|
||||
let cipher_cache = cipher_cache.clone();
|
||||
R.mutation(move |node, args: String| {
|
||||
let cipher_cache = cipher_cache.clone();
|
||||
async move {
|
||||
let cipher = get_cipher(&node, cipher_cache).await?;
|
||||
let en_data = cipher.encrypt(args.as_bytes()).map_err(|e| {
|
||||
error!("Failed to encrypt data: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to encrypt data".to_string(),
|
||||
)
|
||||
})?;
|
||||
let en_data = CookieCipher::base64_encode(&en_data);
|
||||
let base_dir = node.config.data_directory();
|
||||
let path = sanitize_path(&base_dir, Path::new(".sdks"))?;
|
||||
write_file(&path, en_data.as_bytes()).await?;
|
||||
debug!("Saved data to {:?}", path);
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
})
|
||||
.procedure("saveEmailAddress", {
|
||||
R.with2(library())
|
||||
.mutation(move |(node, library), args: String| async move {
|
||||
let path = node
|
||||
.libraries
|
||||
.libraries_dir
|
||||
.join(format!("{}.sdlibrary", library.id));
|
||||
|
||||
let mut config = serde_json::from_slice::<Map<String, Value>>(
|
||||
&tokio::fs::read(path.clone()).await.map_err(|e| {
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
format!("Failed to read library config: {:?}", e.to_string()),
|
||||
)
|
||||
})?,
|
||||
)
|
||||
.map_err(|e: serde_json::Error| {
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
format!("Failed to parse library config: {:?}", e.to_string()),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Encrypt the email address
|
||||
// Create new cipher with the library id as the key
|
||||
let uuid_key =
|
||||
CookieCipher::generate_key_from_string(library.id.to_string().as_str())
|
||||
.map_err(|e| {
|
||||
error!("Failed to generate key: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to generate key".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let cipher = CookieCipher::new(&uuid_key).map_err(|e| {
|
||||
error!("Failed to create cipher: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to create cipher".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let en_data = cipher.encrypt(args.as_bytes()).map_err(|e| {
|
||||
error!("Failed to encrypt data: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to encrypt data".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let en_data = CookieCipher::base64_encode(&en_data);
|
||||
|
||||
config.remove("cloud_email_address");
|
||||
config.insert("cloud_email_address".to_string(), json!(en_data));
|
||||
|
||||
tokio::fs::write(
|
||||
path,
|
||||
serde_json::to_vec(&config).map_err(|e| {
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
format!("Failed to serialize library config: {:?}", e.to_string()),
|
||||
)
|
||||
})?,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
format!("Failed to write library config: {:?}", e.to_string()),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.procedure("getEmailAddress", {
|
||||
R.with2(library())
|
||||
.query(move |(node, library), _: ()| async move {
|
||||
let path = node
|
||||
.libraries
|
||||
.libraries_dir
|
||||
.join(format!("{}.sdlibrary", library.id));
|
||||
|
||||
let config = serde_json::from_slice::<Map<String, Value>>(
|
||||
&tokio::fs::read(path.clone()).await.map_err(|e| {
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
format!("Failed to read library config: {:?}", e.to_string()),
|
||||
)
|
||||
})?,
|
||||
)
|
||||
.map_err(|e: serde_json::Error| {
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
format!("Failed to parse library config: {:?}", e.to_string()),
|
||||
)
|
||||
})?;
|
||||
|
||||
let en_data = config.get("cloud_email_address").ok_or_else(|| {
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to get cloud_email_address".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let en_data = en_data.as_str().ok_or_else(|| {
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to get cloud_email_address".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let en_data = CookieCipher::base64_decode(en_data).map_err(|e| {
|
||||
error!("Failed to decode data: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to decode data".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let uuid_key =
|
||||
CookieCipher::generate_key_from_string(library.id.to_string().as_str())
|
||||
.map_err(|e| {
|
||||
error!("Failed to generate key: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to generate key".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let cipher = CookieCipher::new(&uuid_key).map_err(|e| {
|
||||
error!("Failed to create cipher: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to create cipher".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let de_data = cipher.decrypt(&en_data).map_err(|e| {
|
||||
error!("Failed to decrypt data: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to decrypt data".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let de_data = String::from_utf8(de_data).map_err(|e| {
|
||||
error!("Failed to convert data to string: {:?}", e.to_string());
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to convert data to string".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(de_data)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -29,6 +29,7 @@ mod cloud;
|
||||
mod ephemeral_files;
|
||||
mod files;
|
||||
mod jobs;
|
||||
mod keys;
|
||||
mod labels;
|
||||
mod libraries;
|
||||
pub mod locations;
|
||||
@@ -210,6 +211,7 @@ pub(crate) fn mount() -> Arc<Router> {
|
||||
.merge("preferences.", preferences::mount())
|
||||
.merge("notifications.", notifications::mount())
|
||||
.merge("backups.", backups::mount())
|
||||
.merge("keys.", keys::mount())
|
||||
.merge("invalidation.", utils::mount_invalidate())
|
||||
.sd_patch_types_dangerously(|type_map| {
|
||||
let def =
|
||||
|
||||
@@ -46,6 +46,8 @@ pub struct LibraryConfig {
|
||||
|
||||
#[serde(skip, default)]
|
||||
pub config_path: PathBuf,
|
||||
/// cloud_email_address is the email address of the user who owns the cloud library this library is linked to.
|
||||
pub cloud_email_address: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
@@ -74,10 +76,11 @@ pub enum LibraryConfigVersion {
|
||||
V9 = 9,
|
||||
V10 = 10,
|
||||
V11 = 11,
|
||||
V12 = 12,
|
||||
}
|
||||
|
||||
impl ManagedVersion<LibraryConfigVersion> for LibraryConfig {
|
||||
const LATEST_VERSION: LibraryConfigVersion = LibraryConfigVersion::V11;
|
||||
const LATEST_VERSION: LibraryConfigVersion = LibraryConfigVersion::V12;
|
||||
|
||||
const KIND: Kind = Kind::Json("version");
|
||||
|
||||
@@ -99,6 +102,7 @@ impl LibraryConfig {
|
||||
cloud_id: None,
|
||||
generate_sync_operations: Arc::new(AtomicBool::new(false)),
|
||||
config_path: path.as_ref().to_path_buf(),
|
||||
cloud_email_address: None,
|
||||
};
|
||||
|
||||
this.save(path).await.map(|()| this)
|
||||
@@ -396,6 +400,25 @@ impl LibraryConfig {
|
||||
.await?;
|
||||
}
|
||||
|
||||
(LibraryConfigVersion::V11, LibraryConfigVersion::V12) => {
|
||||
// Add the `cloud_email_address` field to the library config.
|
||||
let mut config = serde_json::from_slice::<Map<String, Value>>(
|
||||
&fs::read(path).await.map_err(|e| {
|
||||
VersionManagerError::FileIO(FileIOError::from((path, e)))
|
||||
})?,
|
||||
)
|
||||
.map_err(VersionManagerError::SerdeJson)?;
|
||||
|
||||
config.insert(String::from("cloud_email_address"), Value::Null);
|
||||
|
||||
fs::write(
|
||||
path,
|
||||
&serde_json::to_vec(&config).map_err(VersionManagerError::SerdeJson)?,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| VersionManagerError::FileIO(FileIOError::from((path, e))))?;
|
||||
}
|
||||
|
||||
_ => {
|
||||
error!(current_version = ?current, "Library config version is not handled;");
|
||||
|
||||
|
||||
@@ -401,13 +401,46 @@ impl NodeConfig {
|
||||
config.remove("sd_api_origin");
|
||||
config.remove("image_labeler_version");
|
||||
|
||||
config.remove("id");
|
||||
config.insert(
|
||||
String::from("id"),
|
||||
serde_json::to_value(DevicePubId::from(Uuid::now_v7()))
|
||||
.map_err(VersionManagerError::SerdeJson)?,
|
||||
);
|
||||
// Verify that the ID isn't already set to a UUID v7. If it is, we don't want to overwrite it.
|
||||
// Get the current ID, if it's a string, parse it as a UUID and check if it's a UUID v7.
|
||||
// If it's not a UUID v7, set it to a UUID v7.
|
||||
let id = config
|
||||
.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| Uuid::parse_str(s).ok());
|
||||
if let Some(id) = id {
|
||||
if id.get_version() != Some(uuid::Version::Md5) {
|
||||
config.remove("id");
|
||||
config.insert(
|
||||
String::from("id"),
|
||||
serde_json::to_value(DevicePubId::from(Uuid::now_v7()))
|
||||
.map_err(VersionManagerError::SerdeJson)?,
|
||||
);
|
||||
}
|
||||
}
|
||||
// config.remove("id");
|
||||
// config.insert(
|
||||
// String::from("id"),
|
||||
// serde_json::to_value(DevicePubId::from(Uuid::now_v7()))
|
||||
// .map_err(VersionManagerError::SerdeJson)?,
|
||||
// );
|
||||
|
||||
// Create a .sdks file in the data directory if it doesn't exist
|
||||
let data_directory = path
|
||||
.parent()
|
||||
.expect("Config path must have a parent directory");
|
||||
let sdks_file = data_directory.join(".sdks");
|
||||
if !sdks_file.exists() {
|
||||
fs::write(&sdks_file, b"").await.map_err(|e| {
|
||||
FileIOError::from((
|
||||
sdks_file.clone(),
|
||||
e,
|
||||
"Failed to create .sdks file",
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Write the updated config back to disk
|
||||
fs::write(
|
||||
path,
|
||||
serde_json::to_vec(&config).map_err(VersionManagerError::SerdeJson)?,
|
||||
|
||||
32
core/src/search/mod.rs
Normal file
32
core/src/search/mod.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
pub enum SpacedrivePath {
|
||||
Location(u64, PathBuf),
|
||||
Virtual(PathBuf),
|
||||
NonIndexed(PathBuf),
|
||||
}
|
||||
|
||||
pub struct ExplorerItem {
|
||||
pub id: u64,
|
||||
pub pub_id: Bytes,
|
||||
pub inode: Option<u64>,
|
||||
// the unique Object for this item
|
||||
pub object_id: Option<u64>,
|
||||
// the path of this item in Spacedrive
|
||||
pub path: SpacedrivePath,
|
||||
|
||||
// metadata about this item
|
||||
pub name: String,
|
||||
pub extension: Option<String>,
|
||||
pub kind: ObjectKind,
|
||||
pub size: Option<u64>,
|
||||
pub date_created: Option<DateTime<Utc>>,
|
||||
pub date_modified: Option<DateTime<Utc>>,
|
||||
pub date_indexed: Option<DateTime<Utc>>,
|
||||
pub is_dir: bool,
|
||||
pub is_hidden: bool,
|
||||
pub key_id: Option<u64>,
|
||||
|
||||
// computed properties
|
||||
pub thumbnail: Option<ThumbKey>,
|
||||
pub has_created_thumbnail: bool,
|
||||
pub duplicate_paths: Vec<SpacedrivePath>,
|
||||
}
|
||||
@@ -122,7 +122,7 @@ impl Volumes {
|
||||
.await
|
||||
.map_err(|_| VolumeError::Cancelled)?;
|
||||
|
||||
rx.await.map_err(|_| VolumeError::Cancelled)?;
|
||||
let _ = rx.await.map_err(|_| VolumeError::Cancelled)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
name = "sd-crypto"
|
||||
version = "0.0.1"
|
||||
|
||||
authors = ["Ericson Soares <ericson@spacedrive.com>", "Jake Robinson <jake@spacedrive.com>"]
|
||||
authors = [
|
||||
"Arnab Chakraborty <arnab@spacedrive.com>",
|
||||
"Ericson Soares <ericson@spacedrive.com>",
|
||||
"Jake Robinson <jake@spacedrive.com>"
|
||||
]
|
||||
description = """
|
||||
A cryptographic library that provides safe and high-level
|
||||
encryption, hashing, and encoding interfaces.
|
||||
@@ -18,12 +22,15 @@ rust-version.workspace = true
|
||||
[dependencies]
|
||||
# Workspace dependencies
|
||||
async-stream = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
blake3 = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["io-util", "macros", "rt-multi-thread", "sync"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-test = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["derive"] }
|
||||
|
||||
# External dependencies
|
||||
|
||||
306
crates/crypto/src/cookie.rs
Normal file
306
crates/crypto/src/cookie.rs
Normal file
@@ -0,0 +1,306 @@
|
||||
//! Encryption and decryption functionality for cookie strings using the `ChaCha20Poly1305` AEAD cipher.
|
||||
//!
|
||||
//! This module provides a secure way to encrypt and decrypt cookie data using the
|
||||
//! `ChaCha20Poly1305` authenticated encryption algorithm. It includes functionality for:
|
||||
//! - Key generation from UUIDs
|
||||
//! - Encryption with random nonces
|
||||
//! - Decryption with authentication
|
||||
//! - Base64 encoding/decoding utilities
|
||||
|
||||
use base64::Engine;
|
||||
use blake3;
|
||||
use chacha20poly1305::{
|
||||
aead::{Aead, AeadCore, KeyInit},
|
||||
ChaCha20Poly1305, Key,
|
||||
};
|
||||
use std::convert::TryFrom;
|
||||
use tracing::{debug, error};
|
||||
|
||||
/// Main struct for handling encryption and decryption operations.
|
||||
/// Contains an initialized `ChaCha20Poly1305` cipher instance.
|
||||
#[derive(Clone)]
|
||||
pub struct CookieCipher {
|
||||
cipher: ChaCha20Poly1305,
|
||||
}
|
||||
|
||||
/// Possible errors that can occur during cryptographic operations.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CryptoCookieError {
|
||||
/// Errors that occur during encryption operations
|
||||
#[error("Encryption failed: {0}")]
|
||||
Encryption(String),
|
||||
/// Errors that occur during decryption operations
|
||||
#[error("Decryption failed: {0}")]
|
||||
Decryption(String),
|
||||
/// Errors that occur during key creation/initialization
|
||||
#[error("Key creation failed: {0}")]
|
||||
KeyCreation(String),
|
||||
}
|
||||
|
||||
impl CookieCipher {
|
||||
/// Creates a new `CookieCipher` instance with the provided 32-byte key.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `key` - A 32-byte array used as the encryption/decryption key
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<Self, CryptoCookieError>` - A new `CookieCipher` instance or an error
|
||||
pub fn new(key: &[u8; 32]) -> Result<Self, CryptoCookieError> {
|
||||
debug!("Initializing CookieCipher with provided key");
|
||||
let key = Key::try_from(key.as_slice()).map_err(|e| {
|
||||
error!("Failed to create key: {}", e);
|
||||
CryptoCookieError::KeyCreation(e.to_string())
|
||||
})?;
|
||||
|
||||
let cipher = ChaCha20Poly1305::new(&key);
|
||||
debug!("CookieCipher initialized successfully");
|
||||
Ok(Self { cipher })
|
||||
}
|
||||
|
||||
/// Encrypts the provided data using `ChaCha20Poly1305`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - The data to encrypt
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<Vec<u8>, CryptoCookieError>` - The encrypted data with prepended nonce, or an error
|
||||
pub fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>, CryptoCookieError> {
|
||||
debug!("Starting encryption of {} bytes", data.len());
|
||||
|
||||
let nonce = ChaCha20Poly1305::generate_nonce_with_rng(&mut aead::OsRng).map_err(|e| {
|
||||
error!("Nonce generation failed: {}", e);
|
||||
CryptoCookieError::Encryption(e.to_string())
|
||||
})?;
|
||||
debug!("Generated new nonce for encryption");
|
||||
|
||||
let ciphertext = self.cipher.encrypt(&nonce, data).map_err(|e| {
|
||||
error!("Encryption failed: {}", e);
|
||||
CryptoCookieError::Encryption(e.to_string())
|
||||
})?;
|
||||
|
||||
let mut combined = nonce.to_vec();
|
||||
combined.extend(ciphertext);
|
||||
|
||||
debug!("Successfully encrypted data to {} bytes", combined.len());
|
||||
Ok(combined)
|
||||
}
|
||||
|
||||
/// Validates that the encrypted data meets the minimum length requirement.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - The encrypted data to validate
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<(), CryptoCookieError>` - Ok if valid, Error if too short
|
||||
fn validate_data_length(data: &[u8]) -> Result<(), CryptoCookieError> {
|
||||
if data.len() < 12 {
|
||||
error!("Encrypted data too short: {} bytes", data.len());
|
||||
return Err(CryptoCookieError::Decryption("Data too short".into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extracts and validates the nonce from the encrypted data.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `nonce_bytes` - The bytes containing the nonce
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<chacha20poly1305::Nonce, CryptoCookieError>` - The extracted nonce or an error
|
||||
fn extract_nonce(nonce_bytes: &[u8]) -> Result<chacha20poly1305::Nonce, CryptoCookieError> {
|
||||
chacha20poly1305::Nonce::try_from(nonce_bytes).map_err(|e| {
|
||||
error!("Failed to create nonce: {}", e);
|
||||
CryptoCookieError::Decryption(e.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// Performs the actual decryption operation.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `nonce` - The nonce to use for decryption
|
||||
/// * `ciphertext` - The encrypted data to decrypt
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<Vec<u8>, CryptoCookieError>` - The decrypted data or an error
|
||||
fn perform_decryption(
|
||||
&self,
|
||||
nonce: &chacha20poly1305::Nonce,
|
||||
ciphertext: &[u8],
|
||||
) -> Result<Vec<u8>, CryptoCookieError> {
|
||||
self.cipher.decrypt(nonce, ciphertext).map_err(|e| {
|
||||
error!("Decryption failed: {}", e);
|
||||
CryptoCookieError::Decryption(e.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// Decrypts the provided encrypted data.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `encrypted_data` - The data to decrypt (including nonce)
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<Vec<u8>, CryptoCookieError>` - The decrypted data or an error
|
||||
pub fn decrypt(&self, encrypted_data: &[u8]) -> Result<Vec<u8>, CryptoCookieError> {
|
||||
debug!("Starting decryption of {} bytes", encrypted_data.len());
|
||||
|
||||
Self::validate_data_length(encrypted_data)?;
|
||||
|
||||
let (nonce_bytes, ciphertext) = encrypted_data.split_at(12);
|
||||
let nonce = Self::extract_nonce(nonce_bytes)?;
|
||||
|
||||
debug!("Extracted nonce and ciphertext for decryption");
|
||||
|
||||
let plaintext = self.perform_decryption(&nonce, ciphertext)?;
|
||||
|
||||
debug!("Successfully decrypted data to {} bytes", plaintext.len());
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
/// Generates a 32-byte key from a string input using BLAKE3 hashing.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `string` - The input string (typically a UUID) to generate the key from
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<[u8; 32], CryptoCookieError>` - A 32-byte key or an error
|
||||
pub fn generate_key_from_string(string: &str) -> Result<[u8; 32], CryptoCookieError> {
|
||||
debug!("Generating key from string: {}", string);
|
||||
|
||||
if string.is_empty() {
|
||||
error!("Input string is empty");
|
||||
return Err(CryptoCookieError::KeyCreation(
|
||||
"Input string is empty".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Hash the input string to get a fixed-size output
|
||||
let hash = blake3::hash(string.as_bytes());
|
||||
|
||||
// Convert the hash bytes directly to an array
|
||||
let key_array: [u8; 32] = *hash.as_bytes();
|
||||
|
||||
debug!("Key generated successfully");
|
||||
Ok(key_array)
|
||||
}
|
||||
|
||||
/// Encodes binary data to base64 string.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - The binary data to encode
|
||||
///
|
||||
/// # Returns
|
||||
/// * `String` - The base64 encoded string
|
||||
#[must_use]
|
||||
pub fn base64_encode(data: &[u8]) -> String {
|
||||
base64::engine::general_purpose::STANDARD.encode(data)
|
||||
}
|
||||
|
||||
/// Decodes base64 string to binary data.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - The base64 string to decode
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<Vec<u8>, base64::DecodeError>` - The decoded binary data or an error
|
||||
pub fn base64_decode(data: &str) -> Result<Vec<u8>, base64::DecodeError> {
|
||||
base64::engine::general_purpose::STANDARD.decode(data)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_key_generation() {
|
||||
let key = CookieCipher::generate_key_from_string("0193b34e-0ad9-70e0-a3dd-8ec30b73a90a")
|
||||
.expect("Failed to generate key");
|
||||
|
||||
assert_eq!(key.len(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encryption_decryption() {
|
||||
let key = CookieCipher::generate_key_from_string("0193b34e-0ad9-70e0-a3dd-8ec30b73a90a")
|
||||
.expect("Failed to generate key");
|
||||
let cipher = CookieCipher::new(&key).expect("Failed to create cipher");
|
||||
|
||||
let data = b"Hello, world!";
|
||||
let encrypted = cipher.encrypt(data).expect("Failed to encrypt data");
|
||||
let decrypted = cipher.decrypt(&encrypted).expect("Failed to decrypt data");
|
||||
|
||||
assert_eq!(data, decrypted.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_base64_encoding() {
|
||||
let data = b"Hello, world!";
|
||||
let encoded = CookieCipher::base64_encode(data);
|
||||
let decoded = CookieCipher::base64_decode(&encoded).expect("Failed to decode base64");
|
||||
|
||||
assert_eq!(data, decoded.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_data_length() {
|
||||
let key = CookieCipher::generate_key_from_string("0193b34e-0ad9-70e0-a3dd-8ec30b73a90a")
|
||||
.expect("Failed to generate key");
|
||||
let cipher = CookieCipher::new(&key).expect("Failed to create cipher");
|
||||
|
||||
let encrypted = vec![0; 10];
|
||||
let result = cipher.decrypt(&encrypted);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_nonce() {
|
||||
let key = CookieCipher::generate_key_from_string("0193b34e-0ad9-70e0-a3dd-8ec30b73a90a")
|
||||
.expect("Failed to generate key");
|
||||
let cipher = CookieCipher::new(&key).expect("Failed to create cipher");
|
||||
|
||||
let encrypted = vec![0; 12];
|
||||
let result = cipher.decrypt(&encrypted);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_decryption() {
|
||||
let key = CookieCipher::generate_key_from_string("0193b34e-0ad9-70e0-a3dd-8ec30b73a90a")
|
||||
.expect("Failed to generate key");
|
||||
let cipher = CookieCipher::new(&key).expect("Failed to create cipher");
|
||||
|
||||
let encrypted = vec![0; 24];
|
||||
let result = cipher.decrypt(&encrypted);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_base64() {
|
||||
let result = CookieCipher::base64_decode("invalid_base64");
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_key_generation() {
|
||||
let result = CookieCipher::generate_key_from_string("");
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_decryption_operation() {
|
||||
let key = CookieCipher::generate_key_from_string("0193b34e-0ad9-70e0-a3dd-8ec30b73a90a")
|
||||
.expect("Failed to generate key");
|
||||
let cipher = CookieCipher::new(&key).expect("Failed to create cipher");
|
||||
|
||||
let nonce = chacha20poly1305::Nonce::default();
|
||||
let ciphertext = vec![0; 1024];
|
||||
let result = cipher.perform_decryption(&nonce, &ciphertext);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -41,3 +41,5 @@ pub use protected::Protected;
|
||||
pub use rng::CryptoRng;
|
||||
|
||||
pub use rand_core::{RngCore, SeedableRng};
|
||||
|
||||
pub mod cookie;
|
||||
|
||||
@@ -232,16 +232,16 @@ fn thumb_scale_filter_args(
|
||||
// if the pixel aspect ratio is defined and is not 1, we have an anamorphic stream
|
||||
if pixel_aspect_ratio.num != 0 && pixel_aspect_ratio.num != pixel_aspect_ratio.den {
|
||||
match std::panic::catch_unwind(|| {
|
||||
width
|
||||
.checked_mul(pixel_aspect_ratio.num.unsigned_abs())
|
||||
.and_then(|v| v.checked_div(pixel_aspect_ratio.den.unsigned_abs()))
|
||||
}) {
|
||||
Ok(Some(w)) => width = w,
|
||||
Ok(None) | Err(_) => {
|
||||
eprintln!("Warning: Failed to calculate width with pixel aspect ratio");
|
||||
// Keep the original width as fallback
|
||||
}
|
||||
};
|
||||
width
|
||||
.checked_mul(pixel_aspect_ratio.num.unsigned_abs())
|
||||
.and_then(|v| v.checked_div(pixel_aspect_ratio.den.unsigned_abs()))
|
||||
}) {
|
||||
Ok(Some(w)) => width = w,
|
||||
Ok(None) | Err(_) => {
|
||||
eprintln!("Warning: Failed to calculate width with pixel aspect ratio");
|
||||
// Keep the original width as fallback
|
||||
}
|
||||
};
|
||||
if size != 0 {
|
||||
if height > width {
|
||||
width = (width * size) / height;
|
||||
|
||||
11
crates/tauri-plugin-cors-fetch/CHANGELOG.md
Normal file
11
crates/tauri-plugin-cors-fetch/CHANGELOG.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# v2.1.0
|
||||
|
||||
- Fix: Exclude Tauri IPC requests from the request hook.
|
||||
|
||||
# v2.0.0
|
||||
|
||||
- New: Hook `fetch` requests and redirect them to [tauri-plugin-http](https://crates.io/crates/tauri-plugin-http).
|
||||
|
||||
# v1.0.0
|
||||
|
||||
- New: Hook `fetch` requests and redirect them to `x-http` and `x-https` custom protocols.
|
||||
33
crates/tauri-plugin-cors-fetch/Cargo.toml
Normal file
33
crates/tauri-plugin-cors-fetch/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
authors = ["Arnab Chakraborty <arnab@spacedrive.com>", "Del Wang <hello@xbox.work>"]
|
||||
description = "Enabling Cross-Origin Resource Sharing (CORS) for Fetch Requests within Tauri applications. Modified to work with Spacedrive and Supertokens for authentication."
|
||||
documentation = "https://docs.rs/crate/tauri-plugin-cors-fetch"
|
||||
edition = "2021"
|
||||
keywords = ["CORS", "fetch", "tauri-plugin", "unofficial"]
|
||||
license = "MIT"
|
||||
links = "tauri-plugin-cors-fetch"
|
||||
name = "tauri-plugin-cors-fetch"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/idootop/tauri-plugin-cors-fetch"
|
||||
rust-version = "1.70"
|
||||
version = "2.1.1-sd-custom"
|
||||
|
||||
[dependencies]
|
||||
http = "0.2"
|
||||
once_cell = "1.19.0"
|
||||
reqwest = "0.11"
|
||||
sd-crypto = { path = "../crypto" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri = "2.0.0-beta.11"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1.36.0", features = ["macros"] }
|
||||
tracing = { workspace = true }
|
||||
url = "2"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-plugin = { version = "2.0.0-beta.9", features = ["build"] }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustc-args = ["--cfg", "docsrs"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
21
crates/tauri-plugin-cors-fetch/LICENSE
Normal file
21
crates/tauri-plugin-cors-fetch/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Del.Wang
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
88
crates/tauri-plugin-cors-fetch/README.md
Normal file
88
crates/tauri-plugin-cors-fetch/README.md
Normal file
@@ -0,0 +1,88 @@
|
||||

|
||||
|
||||
[](https://crates.io/crates/tauri-plugin-cors-fetch)
|
||||
[](https://docs.rs/crate/tauri-plugin-cors-fetch)
|
||||
[](./LICENSE)
|
||||
|
||||
An **unofficial** Tauri plugin that enables seamless cross-origin resource sharing (CORS) for web fetch requests within Tauri applications.
|
||||
|
||||
## Overview
|
||||
|
||||
When building cross-platform desktop applications with [Tauri](https://tauri.app), we often need to access services like [OpenAI](https://openai.com/product) that are restricted by **Cross-Origin Resource Sharing (CORS)** policies in web environments.
|
||||
|
||||
However, on the desktop, we can bypass CORS and access these services directly. While the official [tauri-plugin-http](https://crates.io/crates/tauri-plugin-http) can bypass CORS, it requires modifying your network requests and might not be compatible with third-party dependencies that rely on the standard `fetch` API.
|
||||
|
||||
## How it Works
|
||||
|
||||
This plugin extends the official [tauri-plugin-http](https://crates.io/crates/tauri-plugin-http) by hooking into the browser's native `fetch` method during webpage initialization. It transparently redirects requests to the [tauri-plugin-http](https://crates.io/crates/tauri-plugin-http), allowing you to use the standard `fetch` API without additional code changes or workarounds.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Add the plugin to your Tauri project's dependencies:
|
||||
|
||||
```shell
|
||||
# src-tauri
|
||||
cargo add tauri-plugin-cors-fetch
|
||||
```
|
||||
|
||||
2. Initialize the plugin in your Tauri application setup:
|
||||
|
||||
```rust
|
||||
// src-tauri/main.rs
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_cors_fetch::init())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("failed to run app");
|
||||
}
|
||||
```
|
||||
|
||||
3. Add permissions in your `capabilities` configuration:
|
||||
|
||||
```json
|
||||
// src-tauri/capabilities/default.json
|
||||
{
|
||||
"permissions": ["cors-fetch:default"]
|
||||
}
|
||||
```
|
||||
|
||||
4. Enable `withGlobalTauri` in your Tauri configuration:
|
||||
|
||||
```json
|
||||
// src-tauri/tauri.conf.json
|
||||
{
|
||||
"app": {
|
||||
"withGlobalTauri": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
After installing and initializing the plugin, you can start making `fetch` requests from your Tauri application without encountering CORS-related errors.
|
||||
|
||||
```javascript
|
||||
// Enable CORS for the hooked fetch globally (default is true on app start)
|
||||
window.enableCORSFetch(true);
|
||||
|
||||
// Use the hooked fetch with CORS support
|
||||
fetch('https://example.com/api')
|
||||
.then((response) => response.json())
|
||||
.then((data) => console.log(data))
|
||||
.catch((error) => console.error(error));
|
||||
|
||||
// Use the hooked fetch directly
|
||||
window.hookedFetch('https://example.com/api');
|
||||
|
||||
// Use the original, unhooked fetch
|
||||
window.originalFetch('https://example.com/api');
|
||||
```
|
||||
|
||||
## Limitation
|
||||
|
||||
1. **No Custom CSP Policy Support**: By default, all HTTP/HTTPS requests will be redirected to local native requests.
|
||||
2. **No XMLHttpRequest Support**: The plugin is designed specifically to work with the modern `fetch` API and does not support `XMLHttpRequest` (XHR) requests.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
118
crates/tauri-plugin-cors-fetch/api-iife.js
Normal file
118
crates/tauri-plugin-cors-fetch/api-iife.js
Normal file
@@ -0,0 +1,118 @@
|
||||
class CORSFetch {
|
||||
_requestId = 1;
|
||||
|
||||
constructor() {
|
||||
window.originalFetch = fetch.bind(window);
|
||||
window.hookedFetch = this.hookedFetch.bind(this);
|
||||
this.enableCORS(true);
|
||||
}
|
||||
|
||||
enableCORS(enable) {
|
||||
window.fetch = enable ? window.hookedFetch : window.originalFetch;
|
||||
}
|
||||
|
||||
async hookedFetch(input, init) {
|
||||
const _url = input instanceof Request ? input.url : input.toString();
|
||||
const isHttpRequests = /^https?:\/\//i.test(_url);
|
||||
|
||||
// `ipc://localhost/${path}` and `http://ipc.localhost/${path}` are used for Tauri IPC requests
|
||||
// https://github.com/tauri-apps/tauri/blob/7898b601d14ed62053dd24011fabadf31ec1af45/core/tauri/scripts/core.js#L12
|
||||
const isTauriIpcRequests =
|
||||
/^ipc:\/\/localhost\//i.test(_url) || /^http:\/\/ipc.localhost\//i.test(_url);
|
||||
|
||||
if (!isHttpRequests || isTauriIpcRequests) {
|
||||
return window.originalFetch(input, init);
|
||||
}
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const requestId = this._requestId++;
|
||||
|
||||
const maxRedirections = init?.maxRedirections;
|
||||
const connectTimeout = init?.connectTimeout;
|
||||
const proxy = init?.proxy;
|
||||
|
||||
// Remove these fields before creating the request
|
||||
if (init) {
|
||||
delete init.maxRedirections;
|
||||
delete init.connectTimeout;
|
||||
delete init.proxy;
|
||||
}
|
||||
|
||||
const signal = init?.signal;
|
||||
|
||||
const headers = !init?.headers
|
||||
? []
|
||||
: init.headers instanceof Headers
|
||||
? Array.from(init.headers.entries())
|
||||
: Array.isArray(init.headers)
|
||||
? init.headers
|
||||
: Object.entries(init.headers);
|
||||
|
||||
const mappedHeaders = headers.map(([name, val]) => [
|
||||
name,
|
||||
// we need to ensure we have all values as strings
|
||||
typeof val === 'string' ? val : val.toString()
|
||||
]);
|
||||
|
||||
const req = new Request(input, init);
|
||||
const buffer = await req.arrayBuffer();
|
||||
const reqData = buffer.byteLength ? Array.from(new Uint8Array(buffer)) : null;
|
||||
|
||||
signal?.addEventListener('abort', async (e) => {
|
||||
const error = e.target.reason;
|
||||
this._invoke('plugin:cors-fetch|cancel_cors_request', {
|
||||
requestId
|
||||
}).catch(() => {});
|
||||
reject(error);
|
||||
});
|
||||
|
||||
const {
|
||||
status,
|
||||
statusText,
|
||||
url,
|
||||
body,
|
||||
headers: responseHeaders
|
||||
} = await this._invoke('plugin:cors-fetch|cors_request', {
|
||||
request: {
|
||||
requestId,
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
headers: mappedHeaders,
|
||||
data: reqData,
|
||||
maxRedirections,
|
||||
connectTimeout,
|
||||
proxy
|
||||
}
|
||||
});
|
||||
|
||||
const res = new Response(
|
||||
body instanceof ArrayBuffer && body.byteLength
|
||||
? body
|
||||
: body instanceof Array && body.length
|
||||
? new Uint8Array(body)
|
||||
: null,
|
||||
{
|
||||
headers: responseHeaders,
|
||||
status,
|
||||
statusText
|
||||
}
|
||||
);
|
||||
|
||||
// url is read only but seems like we can do this
|
||||
Object.defineProperty(res, 'url', { value: url });
|
||||
|
||||
resolve(res);
|
||||
});
|
||||
}
|
||||
|
||||
_invoke(cmd, args, options) {
|
||||
if ('__TAURI__' in window) {
|
||||
return window.__TAURI_INTERNALS__.invoke(cmd, args, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(function () {
|
||||
const cf = new CORSFetch();
|
||||
window.enableCORSFetch = cf.enableCORS.bind(cf);
|
||||
})();
|
||||
BIN
crates/tauri-plugin-cors-fetch/banner.png
Normal file
BIN
crates/tauri-plugin-cors-fetch/banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
7
crates/tauri-plugin-cors-fetch/build.rs
Normal file
7
crates/tauri-plugin-cors-fetch/build.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
const COMMANDS: &[&str] = &["cors_request", "cancel_cors_request"];
|
||||
|
||||
fn main() {
|
||||
tauri_plugin::Builder::new(COMMANDS)
|
||||
.global_api_script_path("./api-iife.js")
|
||||
.build();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-cancel-cors-request"
|
||||
description = "Enables the cancel_cors_request command without any pre-configured scope."
|
||||
commands.allow = ["cancel_cors_request"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-cancel-cors-request"
|
||||
description = "Denies the cancel_cors_request command without any pre-configured scope."
|
||||
commands.deny = ["cancel_cors_request"]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-cors-request"
|
||||
description = "Enables the cors_request command without any pre-configured scope."
|
||||
commands.allow = ["cors_request"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-cors-request"
|
||||
description = "Denies the cors_request command without any pre-configured scope."
|
||||
commands.deny = ["cors_request"]
|
||||
@@ -0,0 +1,68 @@
|
||||
## Default Permission
|
||||
|
||||
Allows all fetch operations
|
||||
|
||||
- `allow-cancel-cors-request`
|
||||
- `allow-cors-request`
|
||||
|
||||
## Permission Table
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Identifier</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`cors-fetch:allow-cancel-cors-request`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the cancel_cors_request command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`cors-fetch:deny-cancel-cors-request`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the cancel_cors_request command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`cors-fetch:allow-cors-request`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the cors_request command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`cors-fetch:deny-cors-request`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the cors_request command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
4
crates/tauri-plugin-cors-fetch/permissions/default.toml
Normal file
4
crates/tauri-plugin-cors-fetch/permissions/default.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
"$schema" = "schemas/schema.json"
|
||||
[default]
|
||||
description = "Allows all fetch operations"
|
||||
permissions = ["allow-cancel-cors-request", "allow-cors-request"]
|
||||
325
crates/tauri-plugin-cors-fetch/permissions/schemas/schema.json
Normal file
325
crates/tauri-plugin-cors-fetch/permissions/schemas/schema.json
Normal file
@@ -0,0 +1,325 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PermissionFile",
|
||||
"description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default": {
|
||||
"description": "The default permission set for the plugin",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/DefaultPermission"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"set": {
|
||||
"description": "A list of permissions sets defined",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionSet"
|
||||
}
|
||||
},
|
||||
"permission": {
|
||||
"description": "A list of inlined permissions",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Permission"
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"DefaultPermission": {
|
||||
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1.0
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does. Tauri convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PermissionSet": {
|
||||
"description": "A set of direct permissions grouped together under a new name.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"identifier",
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.",
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionKind"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1.0
|
||||
},
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does. Tauri internal convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"commands": {
|
||||
"description": "Allowed or denied commands when using this permission.",
|
||||
"default": {
|
||||
"allow": [],
|
||||
"deny": []
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Commands"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scope": {
|
||||
"description": "Allowed or denied scoped when using this permission.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Scopes"
|
||||
}
|
||||
]
|
||||
},
|
||||
"platforms": {
|
||||
"description": "Target platforms this permission applies. By default all platforms are affected by this permission.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Target"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Commands": {
|
||||
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Allowed command.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Denied command, which takes priority.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scopes": {
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Data that defines what is allowed by the scope.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Value": {
|
||||
"description": "All supported ACL values.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Represents a null JSON value.",
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`bool`].",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"description": "Represents a valid ACL [`Number`].",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Number"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`String`].",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Represents a list of other [`Value`]s.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Represents a map of [`String`] keys to [`Value`]s.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"Number": {
|
||||
"description": "A valid ACL number.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Represents an [`i64`].",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`f64`].",
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Target": {
|
||||
"description": "Platform target.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "MacOS.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Windows.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"windows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Linux.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Android.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "iOS.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"iOS"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the cancel_cors_request command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-cancel-cors-request"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cancel_cors_request command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-cancel-cors-request"
|
||||
},
|
||||
{
|
||||
"description": "Enables the cors_request command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-cors-request"
|
||||
},
|
||||
{
|
||||
"description": "Denies the cors_request command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-cors-request"
|
||||
},
|
||||
{
|
||||
"description": "Allows all fetch operations",
|
||||
"type": "string",
|
||||
"const": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
434
crates/tauri-plugin-cors-fetch/src/commands.rs
Normal file
434
crates/tauri-plugin-cors-fetch/src/commands.rs
Normal file
@@ -0,0 +1,434 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Source: tauri-plugin-http@2.0.0-beta.3
|
||||
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
|
||||
use http::{header, HeaderName, HeaderValue, Method};
|
||||
use reqwest::{redirect::Policy, NoProxy, RequestBuilder};
|
||||
use sd_crypto::cookie::CookieCipher;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::command;
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::{Error, Result, NODE_DATA_DIR};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RequestConfig {
|
||||
request_id: u64,
|
||||
method: String,
|
||||
url: url::Url,
|
||||
headers: Vec<(String, String)>,
|
||||
data: Option<Vec<u8>>,
|
||||
connect_timeout: Option<u64>,
|
||||
max_redirections: Option<usize>,
|
||||
proxy: Option<Proxy>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FetchResponse {
|
||||
status: u16,
|
||||
status_text: String,
|
||||
headers: Vec<(String, String)>,
|
||||
url: String,
|
||||
body: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use tokio::sync::oneshot;
|
||||
type RequestPool = Arc<std::sync::Mutex<HashMap<u64, oneshot::Sender<()>>>>;
|
||||
static REQUEST_POOL: Lazy<RequestPool> =
|
||||
Lazy::new(|| Arc::new(std::sync::Mutex::new(HashMap::new())));
|
||||
|
||||
#[command]
|
||||
pub fn cancel_cors_request(request_id: u64) {
|
||||
if let Some(tx) = REQUEST_POOL.lock().unwrap().remove(&request_id) {
|
||||
tx.send(()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn cors_request(request: RequestConfig) -> Result<FetchResponse> {
|
||||
let request_id = request.request_id;
|
||||
let (tx, rx) = oneshot::channel();
|
||||
REQUEST_POOL.lock().unwrap().insert(request_id, tx);
|
||||
let request_config = build_request(request)?;
|
||||
let response = get_response(request_config, rx).await;
|
||||
if !REQUEST_POOL.lock().unwrap().contains_key(&request_id) {
|
||||
return Err(Error::RequestCanceled);
|
||||
}
|
||||
REQUEST_POOL.lock().unwrap().remove(&request_id);
|
||||
response
|
||||
}
|
||||
|
||||
pub fn build_request(request_config: RequestConfig) -> Result<RequestBuilder> {
|
||||
debug!("\n=== Starting Request Build Process ===");
|
||||
let RequestConfig {
|
||||
request_id: _,
|
||||
method,
|
||||
url,
|
||||
headers,
|
||||
data,
|
||||
connect_timeout,
|
||||
max_redirections,
|
||||
proxy,
|
||||
} = request_config;
|
||||
|
||||
debug!("\nRequest Details:");
|
||||
debug!(" Method: {}", method);
|
||||
debug!(" URL: {}", url);
|
||||
|
||||
let method = Method::from_bytes(method.as_bytes())?;
|
||||
debug!("\nParsed HTTP method: {}", method);
|
||||
|
||||
let headers: HashMap<String, String> = HashMap::from_iter(headers);
|
||||
debug!("\nHeaders:");
|
||||
for (key, value) in &headers {
|
||||
debug!(" {} = {}", key, value);
|
||||
}
|
||||
|
||||
let mut builder = reqwest::ClientBuilder::new();
|
||||
debug!("\nBuilding Client Configuration:");
|
||||
|
||||
if let Some(timeout) = connect_timeout {
|
||||
debug!(" Connect Timeout: {}ms", timeout);
|
||||
builder = builder.connect_timeout(Duration::from_millis(timeout));
|
||||
}
|
||||
|
||||
if let Some(max_redirections) = max_redirections {
|
||||
debug!(" Redirect Policy:");
|
||||
builder = builder.redirect(if max_redirections == 0 {
|
||||
debug!(" Redirects disabled");
|
||||
Policy::none()
|
||||
} else {
|
||||
debug!(" Max redirects: {}", max_redirections);
|
||||
Policy::limited(max_redirections)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(proxy_config) = proxy {
|
||||
debug!(" Configuring proxy settings");
|
||||
builder = attach_proxy(proxy_config, builder)?;
|
||||
}
|
||||
|
||||
debug!("\nFinalizing Request:");
|
||||
let client = builder.build()?;
|
||||
let mut request = client.request(method.clone(), url.clone());
|
||||
|
||||
debug!("\nSetting Headers:");
|
||||
for (name, value) in &headers {
|
||||
debug!(" Adding: {} = {}", name, value);
|
||||
let name = HeaderName::from_bytes(name.as_bytes())?;
|
||||
let value = HeaderValue::from_bytes(value.as_bytes())?;
|
||||
request = request.header(name, value);
|
||||
}
|
||||
|
||||
if data.is_none() && matches!(method, Method::POST | Method::PUT) {
|
||||
debug!(
|
||||
" Adding empty content-length header for {} request",
|
||||
method
|
||||
);
|
||||
request = request.header(header::CONTENT_LENGTH, HeaderValue::from(0));
|
||||
}
|
||||
|
||||
if headers.contains_key(header::RANGE.as_str()) {
|
||||
debug!(" Range header present - Setting Accept-Encoding: identity");
|
||||
request = request.header(
|
||||
header::ACCEPT_ENCODING,
|
||||
HeaderValue::from_static("identity"),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(data) = data {
|
||||
debug!("\nRequest Body:");
|
||||
debug!(" Size: {} bytes", data.len());
|
||||
request = request.body(data);
|
||||
}
|
||||
|
||||
debug!("\nRequest build completed successfully");
|
||||
debug!("=====================================\n");
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
pub async fn get_response(
|
||||
request: RequestBuilder,
|
||||
rx: oneshot::Receiver<()>,
|
||||
) -> Result<FetchResponse> {
|
||||
debug!("\n=== Starting Response Fetch ===");
|
||||
|
||||
let response_or_none = tokio::select! {
|
||||
_ = rx => {
|
||||
debug!("\nRequest cancelled by receiver");
|
||||
None
|
||||
},
|
||||
res = request.send() => {
|
||||
debug!("\nRequest sent, awaiting response");
|
||||
Some(res)
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(response) = response_or_none {
|
||||
debug!("\nProcessing Response:");
|
||||
match response {
|
||||
Ok(res) => {
|
||||
let status = res.status();
|
||||
debug!(
|
||||
"\nStatus: {} ({})",
|
||||
status.as_u16(),
|
||||
status.canonical_reason().unwrap_or_default()
|
||||
);
|
||||
|
||||
let url = res.url().to_string();
|
||||
debug!("Final URL: {}", url);
|
||||
|
||||
let mut headers = Vec::new();
|
||||
debug!("\nResponse Headers:");
|
||||
for (key, val) in res.headers().iter() {
|
||||
debug!(" {} = {:?}", key.as_str(), val);
|
||||
headers.push((
|
||||
key.as_str().into(),
|
||||
String::from_utf8(val.as_bytes().to_vec())?,
|
||||
));
|
||||
}
|
||||
|
||||
// Create cookies from headers
|
||||
let mut cookie_store: HashMap<String, String> = HashMap::new();
|
||||
|
||||
// Filter based on Supertokens' headers
|
||||
for (key, val) in res.headers().iter() {
|
||||
match key.as_str() {
|
||||
"front-token" => {
|
||||
if val.to_str().map(|v| v == "remove").unwrap_or(false) {
|
||||
debug!("Removing front-token header (value: remove)");
|
||||
continue;
|
||||
}
|
||||
debug!("Adding front-token header");
|
||||
cookie_store.insert(
|
||||
"front-token".to_string(),
|
||||
val.to_str().unwrap_or_default().to_string(),
|
||||
);
|
||||
}
|
||||
"st-access-token" => {
|
||||
if val.to_str().map(|v| v.is_empty()).unwrap_or(false) {
|
||||
debug!("Removing empty st-access-token header");
|
||||
continue;
|
||||
}
|
||||
debug!("Setting st-access-token cookie");
|
||||
cookie_store.insert(
|
||||
"st-access-token".to_string(),
|
||||
val.to_str().unwrap_or_default().to_string(),
|
||||
);
|
||||
}
|
||||
"st-refresh-token" => {
|
||||
if val.to_str().map(|v| v.is_empty()).unwrap_or(false) {
|
||||
debug!("Removing empty st-refresh-token header");
|
||||
continue;
|
||||
}
|
||||
debug!("Setting st-refresh-token cookie");
|
||||
cookie_store.insert(
|
||||
"st-refresh-token".to_string(),
|
||||
val.to_str().unwrap_or_default().to_string(),
|
||||
);
|
||||
}
|
||||
// "set-cookie" => {
|
||||
// if let Ok(cookie_str) = val.to_str() {
|
||||
// if let Some((name, value)) = cookie_str.split_once('=') {
|
||||
// if let Some(value) = value.split(';').next() {
|
||||
// cookie_store.insert(name.trim().to_string(), value.trim().to_string());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
_ => {}
|
||||
}
|
||||
|
||||
debug!(" {} = {:?}", key.as_str(), val);
|
||||
headers.push((
|
||||
key.as_str().into(),
|
||||
String::from_utf8(val.as_bytes().to_vec())?,
|
||||
));
|
||||
}
|
||||
if !cookie_store.is_empty() {
|
||||
let data_dir = NODE_DATA_DIR.get().unwrap().clone();
|
||||
let data_dir = data_dir.join("spacedrive").join("dev");
|
||||
|
||||
let node_config_path = data_dir.join("node_state.sdconfig");
|
||||
let node_config = std::fs::read_to_string(node_config_path).unwrap();
|
||||
|
||||
let node_config: serde_json::Value =
|
||||
serde_json::from_str(&node_config).unwrap();
|
||||
let node_id = node_config["id"]["Uuid"].as_str().unwrap();
|
||||
debug!("Node ID: {:?}", node_id);
|
||||
|
||||
// Create Cipher
|
||||
let key = CookieCipher::generate_key_from_string(node_id).unwrap();
|
||||
let cipher = CookieCipher::new(&key).unwrap();
|
||||
|
||||
// Read .sdks file
|
||||
let sdks_path = data_dir.join(".sdks");
|
||||
let data = std::fs::read(sdks_path.clone()).unwrap();
|
||||
|
||||
let data_str = String::from_utf8(data)
|
||||
.map_err(|e| {
|
||||
error!("Failed to convert data to string: {:?}", e.to_string());
|
||||
})
|
||||
.unwrap();
|
||||
let data = CookieCipher::base64_decode(&data_str)
|
||||
.map_err(|e| {
|
||||
error!("Failed to decode data: {:?}", e.to_string());
|
||||
})
|
||||
.unwrap();
|
||||
let de_data = cipher
|
||||
.decrypt(&data)
|
||||
.map_err(|e| {
|
||||
error!("Failed to decrypt data: {:?}", e.to_string());
|
||||
})
|
||||
.unwrap();
|
||||
let de_data = String::from_utf8(de_data)
|
||||
.map_err(|e| {
|
||||
error!("Failed to convert data to string: {:?}", e.to_string());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
debug!("Decrypted Data: {:?}", de_data);
|
||||
|
||||
debug!("\nCookies:");
|
||||
for (name, value) in &cookie_store {
|
||||
debug!(" {} = {}", name, value);
|
||||
}
|
||||
|
||||
let mut de_data: Vec<String> = serde_json::from_str(&de_data).unwrap();
|
||||
for cookie in &mut de_data {
|
||||
for (name, value) in &cookie_store {
|
||||
if cookie.starts_with(name) {
|
||||
*cookie = format!("{}={};expires=Fri, 31 Dec 9999 23:59:59 GMT;path=/;samesite=lax", name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Updated Cookies: {:?}", de_data);
|
||||
|
||||
// Now, we will encrypt the de_data and save it to the .sdks file
|
||||
let de_data = serde_json::to_string(&de_data).unwrap();
|
||||
let en_data = cipher
|
||||
.encrypt(de_data.as_bytes())
|
||||
.map_err(|e| {
|
||||
error!("Failed to encrypt data: {:?}", e.to_string());
|
||||
})
|
||||
.unwrap();
|
||||
let en_data = CookieCipher::base64_encode(&en_data);
|
||||
|
||||
std::fs::write(sdks_path, en_data).unwrap();
|
||||
}
|
||||
|
||||
debug!("\nReading Response Body...");
|
||||
let body = res.bytes().await;
|
||||
match body {
|
||||
Ok(bytes) => {
|
||||
debug!("Body received: {} bytes", bytes.len());
|
||||
Ok(FetchResponse {
|
||||
status: status.as_u16(),
|
||||
status_text: status.canonical_reason().unwrap_or_default().to_string(),
|
||||
headers,
|
||||
url,
|
||||
body: Some(bytes.to_vec()),
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to read body: {}", e);
|
||||
Err(Error::Network(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Network error: {}", err);
|
||||
Err(Error::Network(err))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("Request was cancelled");
|
||||
Err(Error::RequestCanceled)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Proxy {
|
||||
all: Option<UrlOrConfig>,
|
||||
http: Option<UrlOrConfig>,
|
||||
https: Option<UrlOrConfig>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(untagged)]
|
||||
pub enum UrlOrConfig {
|
||||
Url(String),
|
||||
Config(ProxyConfig),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProxyConfig {
|
||||
url: String,
|
||||
basic_auth: Option<BasicAuth>,
|
||||
no_proxy: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct BasicAuth {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn proxy_creator(
|
||||
url_or_config: UrlOrConfig,
|
||||
proxy_fn: fn(String) -> reqwest::Result<reqwest::Proxy>,
|
||||
) -> reqwest::Result<reqwest::Proxy> {
|
||||
match url_or_config {
|
||||
UrlOrConfig::Url(url) => Ok(proxy_fn(url)?),
|
||||
UrlOrConfig::Config(ProxyConfig {
|
||||
url,
|
||||
basic_auth,
|
||||
no_proxy,
|
||||
}) => {
|
||||
let mut proxy = proxy_fn(url)?;
|
||||
if let Some(basic_auth) = basic_auth {
|
||||
proxy = proxy.basic_auth(&basic_auth.username, &basic_auth.password);
|
||||
}
|
||||
if let Some(no_proxy) = no_proxy {
|
||||
proxy = proxy.no_proxy(NoProxy::from_string(&no_proxy));
|
||||
}
|
||||
Ok(proxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn attach_proxy(
|
||||
proxy: Proxy,
|
||||
mut builder: reqwest::ClientBuilder,
|
||||
) -> crate::Result<reqwest::ClientBuilder> {
|
||||
let Proxy { all, http, https } = proxy;
|
||||
|
||||
if let Some(all) = all {
|
||||
let proxy = proxy_creator(all, reqwest::Proxy::all)?;
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
|
||||
if let Some(http) = http {
|
||||
let proxy = proxy_creator(http, reqwest::Proxy::http)?;
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
|
||||
if let Some(https) = https {
|
||||
let proxy = proxy_creator(https, reqwest::Proxy::https)?;
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
|
||||
Ok(builder)
|
||||
}
|
||||
49
crates/tauri-plugin-cors-fetch/src/error.rs
Normal file
49
crates/tauri-plugin-cors-fetch/src/error.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
Network(#[from] reqwest::Error),
|
||||
#[error(transparent)]
|
||||
Http(#[from] http::Error),
|
||||
#[error(transparent)]
|
||||
HttpInvalidHeaderName(#[from] http::header::InvalidHeaderName),
|
||||
#[error(transparent)]
|
||||
HttpInvalidHeaderValue(#[from] http::header::InvalidHeaderValue),
|
||||
#[error(transparent)]
|
||||
UrlParseError(#[from] url::ParseError),
|
||||
/// HTTP method error.
|
||||
#[error(transparent)]
|
||||
HttpMethod(#[from] http::method::InvalidMethod),
|
||||
#[error("scheme {0} not supported")]
|
||||
SchemeNotSupport(String),
|
||||
#[error("Request canceled")]
|
||||
RequestCanceled,
|
||||
#[error("failed to process data url")]
|
||||
DataUrlError,
|
||||
#[error("failed to decode data url into bytes")]
|
||||
DataUrlDecodeError,
|
||||
#[error(transparent)]
|
||||
Tauri(#[from] tauri::Error),
|
||||
#[error(transparent)]
|
||||
Utf8(#[from] std::string::FromUtf8Error),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
41
crates/tauri-plugin-cors-fetch/src/lib.rs
Normal file
41
crates/tauri-plugin-cors-fetch/src/lib.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! 
|
||||
//!
|
||||
//! Enabling Cross-Origin Resource Sharing (CORS) for Fetch Requests within Tauri applications.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
pub use reqwest;
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Manager, Runtime,
|
||||
};
|
||||
|
||||
pub use error::{Error, Result};
|
||||
mod commands;
|
||||
mod error;
|
||||
|
||||
pub static NODE_DATA_DIR: OnceCell<PathBuf> = OnceCell::new();
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::<R>::new("cors-fetch")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::cors_request,
|
||||
commands::cancel_cors_request,
|
||||
])
|
||||
.setup(|app_handle, _| {
|
||||
let data_dir = app_handle
|
||||
.path()
|
||||
.data_dir()
|
||||
.unwrap_or_else(|_| PathBuf::from("./"));
|
||||
|
||||
NODE_DATA_DIR.set(data_dir).unwrap();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
@@ -22,3 +22,17 @@ Migrations are run by the Prisma migration engine on app launch.
|
||||
The databases file is SQLite and can be opened in any SQL viewer.
|
||||
|
||||

|
||||
|
||||
### Environment Variables
|
||||
|
||||
The following environment variables can be used to control database behavior:
|
||||
|
||||
- `SD_ACCEPT_DATA_LOSS`: When set to `"true"`, allows operations that might result in data loss. This should only be used in development or testing environments where data loss is acceptable.
|
||||
|
||||
- `SD_FORCE_RESET_DB`: When set to `"true"`, forces a reset of the database. This will clear all existing data. Use with extreme caution and only in development environments.
|
||||
|
||||
You can set these variables when running the app, e.g. `SD_ACCEPT_DATA_LOSS=true pnpm tauri dev`.
|
||||
|
||||
These environment variables are primarily used during development and testing. They should not be used in production environments unless you fully understand the implications.
|
||||
|
||||
⚠️ **Warning**: Setting these variables can result in permanent data loss. Use them only in development environments or when you explicitly need to reset or modify database behavior.
|
||||
|
||||
@@ -15,8 +15,8 @@ const SUPPORTED_ICONS = ['Document', 'Code', 'Text', 'Config'];
|
||||
|
||||
const positionConfig: Record<string, string> = {
|
||||
Text: 'flex h-full w-full items-center justify-center',
|
||||
Code: 'flex h-full w-full items-center justify-center',
|
||||
Config: 'flex h-full w-full items-center justify-center'
|
||||
Code: 'flex h-full w-full items-center justify-center pt-[18px]',
|
||||
Config: 'flex h-full w-full items-center justify-center pt-[18px]'
|
||||
};
|
||||
|
||||
const LayeredFileIcon = forwardRef<HTMLImageElement, LayeredFileIconProps>(
|
||||
@@ -51,7 +51,7 @@ const LayeredFileIcon = forwardRef<HTMLImageElement, LayeredFileIconProps>(
|
||||
className={clsx('pointer-events-none absolute bottom-0 right-0', positionClass)}
|
||||
>
|
||||
<Suspense>
|
||||
<IconComponent viewBox="0 0 16 16" height="40%" width="40%" />
|
||||
<IconComponent viewBox="0 0 16 16" height="50%" width="50%" />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Transparent } from '@sd/assets/images';
|
||||
import clsx from 'clsx';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { memo, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
getItemFilePath,
|
||||
getItemObject,
|
||||
humanizeSize,
|
||||
libraryClient,
|
||||
Tag,
|
||||
useExplorerLayoutStore,
|
||||
useLibraryQuery,
|
||||
@@ -11,6 +13,7 @@ import {
|
||||
type ExplorerItem
|
||||
} from '@sd/client';
|
||||
import { useLocale } from '~/hooks';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
||||
import { useExplorerContext } from '../../../Context';
|
||||
import { ExplorerDraggable } from '../../../ExplorerDraggable';
|
||||
@@ -19,6 +22,7 @@ import { FileThumb } from '../../../FilePath/Thumb';
|
||||
import { useFrame } from '../../../FilePath/useFrame';
|
||||
import { explorerStore } from '../../../store';
|
||||
import { useExplorerDraggable } from '../../../useExplorerDraggable';
|
||||
import { useExplorerItemData } from '../../../useExplorerItemData';
|
||||
import { RenamableItemText } from '../../RenamableItemText';
|
||||
import { ViewItem } from '../../ViewItem';
|
||||
import { GridViewItemContext, useGridViewItemContext } from './Context';
|
||||
|
||||
@@ -34,6 +34,11 @@ interface Props {
|
||||
contextMenu?: () => ReactNode;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
useDragAndDrop: () => void;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This component is used in a few routes and acts as the reference demonstration of how to combine
|
||||
* all the elements of the explorer except for the context, which must be used in the parent component.
|
||||
@@ -82,6 +87,8 @@ export default function Explorer(props: PropsWithChildren<Props>) {
|
||||
explorer.settingsStore.showHiddenFiles = !explorer.settingsStore.showHiddenFiles;
|
||||
});
|
||||
|
||||
window.useDragAndDrop();
|
||||
|
||||
useKeyRevealFinder();
|
||||
|
||||
useExplorerDnd();
|
||||
@@ -111,18 +118,13 @@ export default function Explorer(props: PropsWithChildren<Props>) {
|
||||
contextMenu={props.contextMenu ? props.contextMenu() : <ContextMenu />}
|
||||
emptyNotice={
|
||||
props.emptyNotice ?? (
|
||||
<EmptyNotice
|
||||
icon={FolderNotchOpen}
|
||||
message="This folder is empty"
|
||||
/>
|
||||
<EmptyNotice icon={FolderNotchOpen} message="This folder is empty" />
|
||||
)
|
||||
}
|
||||
listViewOptions={{ hideHeaderBorder: true }}
|
||||
scrollPadding={{
|
||||
top: topBar.topBarHeight,
|
||||
bottom: showPathBar
|
||||
? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0)
|
||||
: undefined
|
||||
bottom: showPathBar ? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0) : undefined
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -142,9 +144,7 @@ export default function Explorer(props: PropsWithChildren<Props>) {
|
||||
)}
|
||||
style={{
|
||||
paddingTop: topBar.topBarHeight + 12,
|
||||
bottom: showPathBar
|
||||
? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0)
|
||||
: 0
|
||||
bottom: showPathBar ? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0) : 0
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { ArrowRight, EjectSimple } from '@phosphor-icons/react';
|
||||
import { EjectSimple } from '@phosphor-icons/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import clsx from 'clsx';
|
||||
import { MouseEvent, PropsWithChildren, useMemo } from 'react';
|
||||
import {
|
||||
useBridgeQuery,
|
||||
useLibraryMutation,
|
||||
useLibraryQuery,
|
||||
useLibrarySubscription,
|
||||
Volume
|
||||
} from '@sd/client';
|
||||
import { useLibraryMutation, useLibraryQuery, useLibrarySubscription, Volume } from '@sd/client';
|
||||
import { Button, toast, tw } from '@sd/ui';
|
||||
import { Icon, IconName } from '~/components';
|
||||
import { useLocale } from '~/hooks';
|
||||
@@ -156,9 +150,7 @@ export default function LocalSection() {
|
||||
onTrack={async () => {
|
||||
if (!isTracked && volume.pub_id) {
|
||||
try {
|
||||
await trackVolumeMutation.mutateAsync({
|
||||
volume_id: Array.from(volume.pub_id) // Convert Uint8Array to number[]
|
||||
});
|
||||
await trackVolumeMutation.mutateAsync(volume.pub_id);
|
||||
toast.success('Volume tracked successfully');
|
||||
} catch (error) {
|
||||
toast.error('Failed to track volume');
|
||||
|
||||
@@ -33,7 +33,16 @@ const Profile = ({
|
||||
}) => {
|
||||
const emailName = user.email?.split('@')[0];
|
||||
const capitalizedEmailName = (emailName?.charAt(0).toUpperCase() ?? '') + emailName?.slice(1);
|
||||
const { accessToken, refreshToken } = getTokens();
|
||||
const [accessToken, setAccessToken] = useState('');
|
||||
const [refreshToken, setRefreshToken] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const tokens = await getTokens();
|
||||
setAccessToken(tokens.accessToken);
|
||||
setRefreshToken(tokens.refreshToken);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const cloudBootstrap = useBridgeMutation('cloud.bootstrap');
|
||||
const devices = useBridgeQuery(['cloud.devices.list']);
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
import { CookieHandlerInterface } from 'supertokens-website/utils/cookieHandler/types';
|
||||
|
||||
const frontendCookiesKey = 'frontendCookies';
|
||||
import { nonLibraryClient } from '@sd/client';
|
||||
|
||||
/**
|
||||
* Tauri handles cookies differently than in browser environments. The SuperTokens
|
||||
* SDK uses frontend cookies, to make sure everything work correctly we add custom
|
||||
* cookie handling and store cookies in local storage instead (This is not a problem
|
||||
* since these are frontend cookies and not server side cookies)
|
||||
* cookie handling and store cookies in an encrypted file called `.sdks`. This file
|
||||
* is stored in the data directory, and must be fetched via RSPC due to the encryption
|
||||
* requirements.
|
||||
*/
|
||||
function getCookiesFromStorage(): string {
|
||||
const cookiesFromStorage = window.localStorage.getItem(frontendCookiesKey);
|
||||
let cookiesFromStorage: string = '';
|
||||
|
||||
if (cookiesFromStorage === null) {
|
||||
window.localStorage.setItem(frontendCookiesKey, '[]');
|
||||
nonLibraryClient
|
||||
.query(['keys.get'])
|
||||
.then((response) => {
|
||||
cookiesFromStorage = response;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('Error fetching cookies from storage: ', e);
|
||||
});
|
||||
|
||||
if (cookiesFromStorage.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Because we store cookies in local storage, we need to manually check
|
||||
* Because we store cookies in a single string, we need to split them by
|
||||
* the delimiter `;` and check the `expires=` part of the cookie string
|
||||
* for expiry before returning all cookies
|
||||
*/
|
||||
const cookieArrayInStorage: string[] = JSON.parse(cookiesFromStorage);
|
||||
@@ -25,47 +34,68 @@ function getCookiesFromStorage(): string {
|
||||
|
||||
for (let cookieIndex = 0; cookieIndex < cookieArrayInStorage.length; cookieIndex++) {
|
||||
const currentCookieString = cookieArrayInStorage[cookieIndex];
|
||||
const parts = currentCookieString?.split(';');
|
||||
const parts = currentCookieString?.split(';') ?? [];
|
||||
let expirationString: string = '';
|
||||
|
||||
for (let partIndex = 0; partIndex < parts!.length; partIndex++) {
|
||||
const currentPart = parts![partIndex];
|
||||
for (let partIndex = 0; partIndex < parts.length; partIndex++) {
|
||||
const currentPart = parts[partIndex];
|
||||
|
||||
if (currentPart!.toLocaleLowerCase().includes('expires=')) {
|
||||
expirationString = currentPart!;
|
||||
if (currentPart?.toLocaleLowerCase().includes('expires=')) {
|
||||
expirationString = currentPart;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (expirationString !== '') {
|
||||
const expirationValueString = expirationString.split('=')[1];
|
||||
const expirationDate = new Date(expirationValueString!);
|
||||
const expirationDate = expirationValueString ? new Date(expirationValueString) : null;
|
||||
const currentTimeInMillis = Date.now();
|
||||
|
||||
// if the cookie has expired, we skip it
|
||||
if (expirationDate.getTime() < currentTimeInMillis) {
|
||||
if (expirationDate && expirationDate.getTime() < currentTimeInMillis) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
cookieArrayToReturn.push(currentCookieString!);
|
||||
if (currentCookieString !== undefined) {
|
||||
cookieArrayToReturn.push(currentCookieString);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After processing and removing expired cookies we need to update the cookies
|
||||
* in storage so we dont have to process the expired ones again
|
||||
*/
|
||||
window.localStorage.setItem(frontendCookiesKey, JSON.stringify(cookieArrayToReturn));
|
||||
// window.localStorage.setItem(frontendCookiesKey, JSON.stringify(cookieArrayToReturn));
|
||||
try {
|
||||
nonLibraryClient.mutation(['keys.save', JSON.stringify(cookieArrayToReturn)]);
|
||||
// console.log("Cookies set successfully");
|
||||
} catch (e) {
|
||||
console.error('Error setting cookies to storage: ', e);
|
||||
}
|
||||
|
||||
return cookieArrayToReturn.join('; ');
|
||||
}
|
||||
|
||||
function setCookieToStorage(cookieString: string) {
|
||||
const cookieName = cookieString.split(';')[0]!.split('=')[0];
|
||||
const cookiesFromStorage = window.localStorage.getItem(frontendCookiesKey);
|
||||
async function setCookieToStorage(cookieString: string) {
|
||||
const cookieName = cookieString.split(';')[0]?.split('=')[0];
|
||||
|
||||
let cookiesFromStorage: string = '';
|
||||
try {
|
||||
const response = await nonLibraryClient.query(['keys.get']);
|
||||
// Debugging
|
||||
const cookiesArrayFromStorage: string[] = JSON.parse(response);
|
||||
// console.log("Cookies fetched from storage: ", cookiesArrayFromStorage);
|
||||
|
||||
// Actual
|
||||
cookiesFromStorage = response;
|
||||
} catch (e) {
|
||||
console.error('Error fetching cookies from storage: ', e);
|
||||
}
|
||||
|
||||
let cookiesArray: string[] = [];
|
||||
|
||||
if (cookiesFromStorage !== null) {
|
||||
if (cookiesFromStorage.length !== 0) {
|
||||
const cookiesArrayFromStorage: string[] = JSON.parse(cookiesFromStorage);
|
||||
cookiesArray = cookiesArrayFromStorage;
|
||||
}
|
||||
@@ -75,7 +105,7 @@ function setCookieToStorage(cookieString: string) {
|
||||
for (let i = 0; i < cookiesArray.length; i++) {
|
||||
const currentCookie = cookiesArray[i];
|
||||
|
||||
if (currentCookie!.indexOf(`${cookieName}=`) !== -1) {
|
||||
if (currentCookie?.indexOf(`${cookieName}=`) !== -1) {
|
||||
cookieIndex = i;
|
||||
break;
|
||||
}
|
||||
@@ -93,7 +123,15 @@ function setCookieToStorage(cookieString: string) {
|
||||
cookiesArray.push(cookieString);
|
||||
}
|
||||
|
||||
window.localStorage.setItem(frontendCookiesKey, JSON.stringify(cookiesArray));
|
||||
try {
|
||||
await nonLibraryClient.mutation(['keys.save', JSON.stringify(cookiesArray)]);
|
||||
// console.log("Cookies set successfully");
|
||||
} catch (e) {
|
||||
console.error('Error setting cookies to storage: ', e);
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log("Setting cookies to storage: ", cookiesArray);
|
||||
}
|
||||
|
||||
export default function getCookieHandler(original: CookieHandlerInterface): CookieHandlerInterface {
|
||||
@@ -104,7 +142,7 @@ export default function getCookieHandler(original: CookieHandlerInterface): Cook
|
||||
return cookies;
|
||||
},
|
||||
setCookie: async function (cookieString: string) {
|
||||
setCookieToStorage(cookieString);
|
||||
await setCookieToStorage(cookieString);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { WindowHandlerInterface } from 'supertokens-website/utils/windowHandler/types';
|
||||
|
||||
/**
|
||||
* This example app uses HashRouter from react-router-dom. The SuperTokens SDK relies on
|
||||
* some window properties like location hash, query params etc. Because HashRouter places
|
||||
* everything other than the website base in the location hash, we need to add custom
|
||||
* handling for some of the properties of the Window API
|
||||
* The SuperTokens SDK relies on some window properties like location hash, query params etc.
|
||||
* This handler is used to override the default window object and provide custom implementations
|
||||
* for these properties.
|
||||
*/
|
||||
export default function getWindowHandler(original: WindowHandlerInterface): WindowHandlerInterface {
|
||||
return {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useBridgeMutation } from '@sd/client';
|
||||
import { Button } from '@sd/ui';
|
||||
import { Authentication } from '~/components';
|
||||
import { useLocale } from '~/hooks';
|
||||
import { AUTH_SERVER_URL } from '~/util';
|
||||
import { AUTH_SERVER_URL, getTokens } from '~/util';
|
||||
|
||||
import { Heading } from '../../Layout';
|
||||
import Profile from './Profile';
|
||||
@@ -24,11 +24,16 @@ export const Component = () => {
|
||||
|
||||
useEffect(() => {
|
||||
async function _() {
|
||||
const tokens = await getTokens();
|
||||
const user_data = await fetch(`${AUTH_SERVER_URL}/api/user`, {
|
||||
method: 'GET'
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens.accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
const data = await user_data.json();
|
||||
// console.log(data);
|
||||
|
||||
setUserInfo(data.id ? data : null);
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ export const Component = () => {
|
||||
</div>
|
||||
<div className="mb-1">
|
||||
<Label className="text-sm font-medium text-ink-faint">
|
||||
<Database className="mr-1 mt-[-2px] inline h-4 w-4" /> Logs Folder
|
||||
<Database className="mr-1 mt-[-2px] inline size-4" /> Logs Folder
|
||||
</Label>
|
||||
<Input value={node.data?.data_path + '/logs'} />
|
||||
</div>
|
||||
|
||||
@@ -136,7 +136,7 @@ export const Authentication = ({
|
||||
{activeTab === 'Login' ? (
|
||||
<Login reload={reload} cloudBootstrap={cloudBootstrap} />
|
||||
) : (
|
||||
<Register />
|
||||
<Register reload={reload} cloudBootstrap={cloudBootstrap} />
|
||||
)}
|
||||
<div className="text-center text-sm text-ink-faint">
|
||||
Social auth and SSO (Single Sign On) available soon!
|
||||
|
||||
@@ -2,11 +2,11 @@ import { ArrowLeft } from '@phosphor-icons/react';
|
||||
import { RSPCError } from '@spacedrive/rspc-client';
|
||||
import { UseMutationResult } from '@tanstack/react-query';
|
||||
import clsx from 'clsx';
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { signIn } from 'supertokens-web-js/recipe/emailpassword';
|
||||
import { createCode } from 'supertokens-web-js/recipe/passwordless';
|
||||
import { useZodForm } from '@sd/client';
|
||||
import { useLibraryMutation, useLibraryQuery, useZodForm } from '@sd/client';
|
||||
import { Button, Divider, Form, Input, toast, z } from '@sd/ui';
|
||||
import { useLocale } from '~/hooks';
|
||||
import { getTokens } from '~/util';
|
||||
@@ -17,7 +17,8 @@ async function signInClicked(
|
||||
email: string,
|
||||
password: string,
|
||||
reload: Dispatch<SetStateAction<boolean>>,
|
||||
cloudBootstrap: UseMutationResult<null, RSPCError, [string, string], unknown> // Cloud bootstrap mutation
|
||||
cloudBootstrap: UseMutationResult<null, RSPCError, [string, string], unknown>, // Cloud bootstrap mutation
|
||||
saveEmailAddress: UseMutationResult<null, RSPCError, string, unknown> // Save email mutation
|
||||
) {
|
||||
try {
|
||||
const response = await signIn({
|
||||
@@ -44,9 +45,9 @@ async function signInClicked(
|
||||
} else if (response.status === 'SIGN_IN_NOT_ALLOWED') {
|
||||
toast.error(response.reason);
|
||||
} else {
|
||||
const tokens = getTokens();
|
||||
console.log(cloudBootstrap);
|
||||
const tokens = await getTokens();
|
||||
cloudBootstrap.mutate([tokens.accessToken, tokens.refreshToken]);
|
||||
saveEmailAddress.mutate(email);
|
||||
toast.success('Sign in successful');
|
||||
reload(true);
|
||||
}
|
||||
@@ -112,18 +113,40 @@ interface LoginProps {
|
||||
const LoginForm = ({ reload, cloudBootstrap, setContinueWithEmail }: LoginProps) => {
|
||||
const { t } = useLocale();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const savedEmailAddress = useLibraryQuery(['keys.getEmailAddress']);
|
||||
const saveEmailAddress = useLibraryMutation(['keys.saveEmailAddress']);
|
||||
|
||||
const form = useZodForm({
|
||||
schema: LoginSchema,
|
||||
defaultValues: {
|
||||
email: '',
|
||||
email: savedEmailAddress.data ?? '',
|
||||
password: ''
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
savedEmailAddress.refetch();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (savedEmailAddress.data) {
|
||||
form.reset({
|
||||
email: savedEmailAddress.data,
|
||||
password: ''
|
||||
});
|
||||
}
|
||||
}, [savedEmailAddress.data]);
|
||||
|
||||
return (
|
||||
<Form
|
||||
onSubmit={form.handleSubmit(async (data) => {
|
||||
await signInClicked(data.email, data.password, reload, cloudBootstrap);
|
||||
await signInClicked(
|
||||
data.email,
|
||||
data.password,
|
||||
reload,
|
||||
cloudBootstrap,
|
||||
saveEmailAddress
|
||||
);
|
||||
})}
|
||||
className="w-full"
|
||||
form={form}
|
||||
@@ -194,7 +217,13 @@ const LoginForm = ({ reload, cloudBootstrap, setContinueWithEmail }: LoginProps)
|
||||
variant="accent"
|
||||
size="md"
|
||||
onClick={form.handleSubmit(async (data) => {
|
||||
await signInClicked(data.email, data.password, reload, cloudBootstrap);
|
||||
await signInClicked(
|
||||
data.email,
|
||||
data.password,
|
||||
reload,
|
||||
cloudBootstrap,
|
||||
saveEmailAddress
|
||||
);
|
||||
})}
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { RSPCError } from '@spacedrive/rspc-client';
|
||||
import { UseMutationResult } from '@tanstack/react-query';
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { signUp } from 'supertokens-web-js/recipe/emailpassword';
|
||||
import { Button, Form, Input, toast, z } from '@sd/ui';
|
||||
import { useLocale } from '~/hooks';
|
||||
import { useLibraryMutation } from '@sd/client';
|
||||
|
||||
import ShowPassword from './ShowPassword';
|
||||
import { getTokens } from '~/util';
|
||||
|
||||
const RegisterSchema = z
|
||||
.object({
|
||||
@@ -26,7 +30,13 @@ const RegisterSchema = z
|
||||
});
|
||||
type RegisterType = z.infer<typeof RegisterSchema>;
|
||||
|
||||
async function signUpClicked(email: string, password: string) {
|
||||
async function signUpClicked(
|
||||
email: string,
|
||||
password: string,
|
||||
reload: Dispatch<SetStateAction<boolean>>,
|
||||
cloudBootstrap: UseMutationResult<null, RSPCError, [string, string], unknown>,
|
||||
saveEmailAddress: UseMutationResult<null, RSPCError, string, unknown>
|
||||
) {
|
||||
try {
|
||||
const response = await signUp({
|
||||
formFields: [
|
||||
@@ -62,9 +72,11 @@ async function signUpClicked(email: string, password: string) {
|
||||
} else {
|
||||
// sign up successful. The session tokens are automatically handled by
|
||||
// the frontend SDK.
|
||||
const tokens = await getTokens();
|
||||
cloudBootstrap.mutate([tokens.accessToken, tokens.refreshToken]);
|
||||
saveEmailAddress.mutate(email);
|
||||
toast.success('Sign up successful');
|
||||
// FIXME: This is a temporary workaround. We will provide a better way to handle this.
|
||||
window.location.reload();
|
||||
reload(true);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.isSuperTokensGeneralError === true) {
|
||||
@@ -76,7 +88,13 @@ async function signUpClicked(email: string, password: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const Register = () => {
|
||||
const Register = ({
|
||||
reload,
|
||||
cloudBootstrap
|
||||
}: {
|
||||
reload: Dispatch<SetStateAction<boolean>>;
|
||||
cloudBootstrap: UseMutationResult<null, RSPCError, [string, string], unknown>; // Cloud bootstrap mutation
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
// useZodForm seems to be out-dated or needs
|
||||
@@ -89,12 +107,13 @@ const Register = () => {
|
||||
confirmPassword: ''
|
||||
}
|
||||
});
|
||||
const savedEmailAddress = useLibraryMutation(['keys.saveEmailAddress']);
|
||||
|
||||
return (
|
||||
<Form
|
||||
onSubmit={form.handleSubmit(async (data) => {
|
||||
// handle sign-up submission
|
||||
console.log(data);
|
||||
await signUpClicked(data.email, data.password);
|
||||
await signUpClicked(data.email, data.password, reload, cloudBootstrap, savedEmailAddress);
|
||||
})}
|
||||
className="w-full"
|
||||
form={form}
|
||||
@@ -190,8 +209,7 @@ const Register = () => {
|
||||
size="md"
|
||||
variant="accent"
|
||||
onClick={form.handleSubmit(async (data) => {
|
||||
console.log(data);
|
||||
await signUpClicked(data.email, data.password);
|
||||
await signUpClicked(data.email, data.password, reload, cloudBootstrap, savedEmailAddress);
|
||||
})}
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import { ArrowRight } from '@phosphor-icons/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { HardwareModel, useBridgeMutation, useZodForm } from '@sd/client';
|
||||
import {
|
||||
CloudDevice,
|
||||
CloudP2PNotifyUser,
|
||||
CloudP2PTicket,
|
||||
CloudSyncGroupWithDevices,
|
||||
HardwareModel,
|
||||
useBridgeMutation,
|
||||
useZodForm
|
||||
} from '@sd/client';
|
||||
import { Dialog, toast, useDialog, UseDialogProps, z } from '@sd/ui';
|
||||
import { Icon } from '~/components';
|
||||
import { useLocale } from '~/hooks';
|
||||
import { hardwareModelToIcon } from '~/util/hardware';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
||||
type ReceivedJoinRequest = Extract<CloudP2PNotifyUser, { kind: 'ReceivedJoinSyncGroupRequest' }>;
|
||||
|
||||
export default (
|
||||
props: {
|
||||
device_name: string;
|
||||
device_model: HardwareModel;
|
||||
library_name: string;
|
||||
data: ReceivedJoinRequest['data'];
|
||||
} & UseDialogProps
|
||||
) => {
|
||||
// PROPS = device_name, device_model, library_name
|
||||
@@ -27,24 +35,40 @@ export default (
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const form = useZodForm({ defaultValues: { libraryId: 'select_library' } });
|
||||
const userResponse = useBridgeMutation('cloud.userResponse');
|
||||
|
||||
// adapted from another dialog - we can change the form submit/remove form if needed but didn't want to
|
||||
// unnecessarily remove code
|
||||
const onSubmit = form.handleSubmit(async (data) => {
|
||||
const onSubmit = form.handleSubmit(async (_d) => {
|
||||
try {
|
||||
// const library = await joinLibrary.mutateAsync(data.libraryId);
|
||||
const library = { uuid: '1234' }; // dummy data
|
||||
userResponse.mutate({
|
||||
kind: 'AcceptDeviceInSyncGroup',
|
||||
data: {
|
||||
ticket: props.data.ticket,
|
||||
accepted: {
|
||||
id: props.data.sync_group.library.pub_id,
|
||||
name: props.data.sync_group.library.name,
|
||||
description: null
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
queryClient.setQueryData(['library.list'], (libraries: any) => {
|
||||
// The invalidation system beat us to it
|
||||
if ((libraries || []).find((l: any) => l.uuid === library.uuid)) return libraries;
|
||||
if (
|
||||
(libraries || []).find(
|
||||
(l: any) => l.uuid === props.data.sync_group.library.pub_id
|
||||
)
|
||||
)
|
||||
return libraries;
|
||||
|
||||
return [...(libraries || []), library];
|
||||
return [...(libraries || []), props.data.sync_group.library];
|
||||
});
|
||||
|
||||
if (platform.refreshMenuBar) platform.refreshMenuBar();
|
||||
|
||||
navigate(`/${library.uuid}`, { replace: true });
|
||||
navigate(`/${props.data.sync_group.library.pub_id}`, { replace: true });
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
toast.error(e);
|
||||
@@ -71,12 +95,12 @@ export default (
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<Icon
|
||||
// once backend endpoint is populated need to check if this is working correctly i.e fetching correct icons for devices
|
||||
name={hardwareModelToIcon(props.device_model)}
|
||||
name={hardwareModelToIcon(props.data.asking_device.hardware_model)}
|
||||
alt="Device icon"
|
||||
size={48}
|
||||
className="mr-2"
|
||||
/>
|
||||
<p className="text-sm text-ink-dull">{props.device_name}</p>
|
||||
<p className="text-sm text-ink-dull">{props.data.asking_device.name}</p>
|
||||
</div>
|
||||
<ArrowRight color="#ABACBA" size={18}></ArrowRight>
|
||||
{/* library */}
|
||||
@@ -88,7 +112,7 @@ export default (
|
||||
size={48}
|
||||
className="mr-2"
|
||||
/>
|
||||
<p className="text-sm text-ink-dull">{props.library_name}</p>
|
||||
<p className="text-sm text-ink-dull">{props.data.sync_group.library.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
@@ -96,27 +96,7 @@ export function SpacedriveInterfaceRoot({ children }: PropsWithChildren) {
|
||||
console.log('Received cloud service notification', d);
|
||||
switch (d.kind) {
|
||||
case 'ReceivedJoinSyncGroupRequest':
|
||||
// WARNING: This is a debug solution to accept the device into the sync group. THIS SHOULD NOT MAKE IT TO PRODUCTION
|
||||
userResponse.mutate({
|
||||
kind: 'AcceptDeviceInSyncGroup',
|
||||
data: {
|
||||
ticket: d.data.ticket,
|
||||
accepted: {
|
||||
id: d.data.sync_group.library.pub_id,
|
||||
name: d.data.sync_group.library.name,
|
||||
description: null
|
||||
}
|
||||
}
|
||||
});
|
||||
// TODO: Move the code above into the dialog below (@Rocky43007)
|
||||
// dialogManager.create((dp) => (
|
||||
// <RequestAddDialog
|
||||
// device_model={'MacBookPro'}
|
||||
// device_name={"Arnab's Macbook"}
|
||||
// library_name={"Arnab's Library"}
|
||||
// {...dp}
|
||||
// />
|
||||
// ));
|
||||
dialogManager.create((dp) => <RequestAddDialog data={d.data} {...dp} />);
|
||||
break;
|
||||
default:
|
||||
toast({ title: 'Cloud Service Notification', body: d.kind }, { type: 'info' });
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"app_crashed_description": "出现了一些错误...",
|
||||
"appearance": "外观",
|
||||
"appearance_description": "调整客户端的外观。",
|
||||
"apply": "申请",
|
||||
"apply": "应用",
|
||||
"archive": "存档",
|
||||
"archive_coming_soon": "存档位置功能即将推出……",
|
||||
"archive_info": "将库中的数据作为存档提取,有利于保留位置的目录结构。",
|
||||
@@ -225,7 +225,7 @@
|
||||
"export_library": "导出库",
|
||||
"export_library_coming_soon": "导出库功能即将推出",
|
||||
"export_library_description": "将这个库导出到一个文件。",
|
||||
"extension": "扩大",
|
||||
"extension": "扩展名",
|
||||
"extensions": "扩展",
|
||||
"extensions_description": "安装扩展来扩展这个客户端的功能。",
|
||||
"fahrenheit": "华氏度",
|
||||
@@ -301,7 +301,7 @@
|
||||
"grid_gap": "间隙",
|
||||
"grid_view": "网格视图",
|
||||
"grid_view_notice_description": "网格视图以缩略图形式显示文件和文件夹,以便直观、快速识别要寻找的文件。",
|
||||
"hidden": "隐",
|
||||
"hidden": "已隐藏",
|
||||
"hidden_label": "阻止位置及其内容出现在汇总分类、搜索和标签中,除非启用了“显示隐藏项目”。",
|
||||
"hide_in_library_search": "在库搜索中隐藏",
|
||||
"hide_in_library_search_description": "在搜索整个库时从结果中隐藏带有此标签的文件。",
|
||||
@@ -390,7 +390,7 @@
|
||||
"local": "本地",
|
||||
"local_locations": "本地位置",
|
||||
"local_node": "本地节点",
|
||||
"location": "地点",
|
||||
"location": "位置",
|
||||
"location_added_successfully": "位置添加成功。",
|
||||
"location_connected_tooltip": "位置正在监视变化",
|
||||
"location_deleted_successfully": "位置删除成功。",
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@icons-pack/react-simple-icons": "^9.1.0",
|
||||
"@spacedrive/rspc-client": "github:spacedriveapp/rspc#path:packages/client&6a77167495",
|
||||
"@phosphor-icons/react": "^2.1.7",
|
||||
"@phosphor-icons/react": "^2.0.13",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-progress": "^1.0.1",
|
||||
@@ -27,6 +26,7 @@
|
||||
"@sd/client": "workspace:*",
|
||||
"@sd/ui": "workspace:*",
|
||||
"@sentry/browser": "^7.74.1",
|
||||
"@spacedrive/rspc-client": "github:spacedriveapp/rspc#path:packages/client&6a77167495",
|
||||
"@tanstack/react-query": "^5.59",
|
||||
"@tanstack/react-query-devtools": "^5.59",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import cryptoRandomString from 'crypto-random-string';
|
||||
import { nonLibraryClient } from '@sd/client';
|
||||
|
||||
// NOTE: `crypto` module is not available in RN so this can't be in client
|
||||
export const generatePassword = (length: number) =>
|
||||
@@ -12,27 +13,20 @@ export const isNonEmptyObject = (input: object) => Object.keys(input).length > 0
|
||||
export const AUTH_SERVER_URL = 'https://auth.spacedrive.com';
|
||||
// export const AUTH_SERVER_URL = 'http://localhost:9420';
|
||||
|
||||
export function getTokens() {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
refreshToken: '',
|
||||
accessToken: ''
|
||||
};
|
||||
}
|
||||
export async function getTokens(): Promise<{ accessToken: string; refreshToken: string }> {
|
||||
const tokens = await nonLibraryClient.query(['keys.get']);
|
||||
const tokensArray = JSON.parse(tokens);
|
||||
|
||||
const refreshToken: string =
|
||||
JSON.parse(window.localStorage.getItem('frontendCookies') ?? '[]')
|
||||
tokensArray
|
||||
.find((cookie: string) => cookie.startsWith('st-refresh-token'))
|
||||
?.split('=')[1]
|
||||
.split(';')[0] || '';
|
||||
const accessToken: string =
|
||||
JSON.parse(window.localStorage.getItem('frontendCookies') ?? '[]')
|
||||
tokensArray
|
||||
.find((cookie: string) => cookie.startsWith('st-access-token'))
|
||||
?.split('=')[1]
|
||||
.split(';')[0] || '';
|
||||
|
||||
return {
|
||||
refreshToken,
|
||||
accessToken
|
||||
};
|
||||
return { accessToken, refreshToken };
|
||||
}
|
||||
|
||||
BIN
packages/assets/images/Transparent.png
Normal file
BIN
packages/assets/images/Transparent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
4
packages/assets/images/index.ts
generated
4
packages/assets/images/index.ts
generated
@@ -14,6 +14,7 @@ import Dropbox from './Dropbox.png';
|
||||
import GoogleDrive from './GoogleDrive.png';
|
||||
import iCloud from './iCloud.png';
|
||||
import Mega from './Mega.png';
|
||||
import Transparent from './Transparent.png';
|
||||
|
||||
export {
|
||||
AlphaBg,
|
||||
@@ -26,5 +27,6 @@ export {
|
||||
Dropbox,
|
||||
GoogleDrive,
|
||||
Mega,
|
||||
iCloud
|
||||
iCloud,
|
||||
Transparent
|
||||
};
|
||||
|
||||
30
packages/client/src/core.ts
generated
30
packages/client/src/core.ts
generated
@@ -24,6 +24,8 @@ export type Procedures = {
|
||||
{ key: "invalidation.test-invalidate", input: never, result: number } |
|
||||
{ key: "jobs.isActive", input: LibraryArgs<null>, result: boolean } |
|
||||
{ key: "jobs.reports", input: LibraryArgs<null>, result: JobGroup[] } |
|
||||
{ key: "keys.get", input: never, result: string } |
|
||||
{ key: "keys.getEmailAddress", input: LibraryArgs<null>, result: string } |
|
||||
{ key: "labels.count", input: LibraryArgs<null>, result: number } |
|
||||
{ key: "labels.get", input: LibraryArgs<number>, result: Label | null } |
|
||||
{ key: "labels.getForObject", input: LibraryArgs<number>, result: Label[] } |
|
||||
@@ -108,6 +110,8 @@ export type Procedures = {
|
||||
{ key: "jobs.objectValidator", input: LibraryArgs<ObjectValidatorArgs>, result: null } |
|
||||
{ key: "jobs.pause", input: LibraryArgs<string>, result: null } |
|
||||
{ key: "jobs.resume", input: LibraryArgs<string>, result: null } |
|
||||
{ key: "keys.save", input: string, result: null } |
|
||||
{ key: "keys.saveEmailAddress", input: LibraryArgs<string>, result: null } |
|
||||
{ key: "labels.delete", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "library.create", input: CreateLibraryArgs, result: LibraryConfigWrapped } |
|
||||
{ key: "library.delete", input: string, result: null } |
|
||||
@@ -138,7 +142,7 @@ export type Procedures = {
|
||||
{ key: "tags.delete", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "tags.update", input: LibraryArgs<TagUpdateArgs>, result: null } |
|
||||
{ key: "toggleFeatureFlag", input: BackendFeature, result: null } |
|
||||
{ key: "volumes.track", input: LibraryArgs<TrackVolumeInput>, result: null } |
|
||||
{ key: "volumes.track", input: LibraryArgs<VolumeFingerprint>, result: null } |
|
||||
{ key: "volumes.unmount", input: LibraryArgs<number[]>, result: null },
|
||||
subscriptions:
|
||||
{ key: "cloud.listenCloudServicesNotifications", input: never, result: CloudP2PNotifyUser } |
|
||||
@@ -524,9 +528,13 @@ instance_id: number;
|
||||
* cloud_id is the ID of the cloud library this library is linked to.
|
||||
* If this is set we can assume the library is synced with the Cloud.
|
||||
*/
|
||||
cloud_id?: string | null; generate_sync_operations?: boolean; version: LibraryConfigVersion }
|
||||
cloud_id?: string | null; generate_sync_operations?: boolean; version: LibraryConfigVersion;
|
||||
/**
|
||||
* cloud_email_address is the email address of the user who owns the cloud library this library is linked to.
|
||||
*/
|
||||
cloud_email_address: string | null }
|
||||
|
||||
export type LibraryConfigVersion = "V0" | "V1" | "V2" | "V3" | "V4" | "V5" | "V6" | "V7" | "V8" | "V9" | "V10" | "V11"
|
||||
export type LibraryConfigVersion = "V0" | "V1" | "V2" | "V3" | "V4" | "V5" | "V6" | "V7" | "V8" | "V9" | "V10" | "V11" | "V12"
|
||||
|
||||
export type LibraryConfigWrapped = { uuid: string; instance_id: string; instance_public_key: RemoteIdentity; config: LibraryConfig }
|
||||
|
||||
@@ -790,8 +798,6 @@ export type TextMatch = { contains: string } | { startsWith: string } | { endsWi
|
||||
*/
|
||||
export type ThumbKey = { shard_hex: string; cas_id: CasId; base_directory_str: string }
|
||||
|
||||
export type TrackVolumeInput = { volume_id: VolumeFingerprint }
|
||||
|
||||
export type UpdateThumbnailerPreferences = Record<string, never>
|
||||
|
||||
export type VideoProps = { pixel_format: string | null; color_range: string | null; bits_per_channel: number | null; color_space: string | null; color_primaries: string | null; color_transfer: string | null; field_order: string | null; chroma_location: string | null; width: number; height: number; aspect_ratio_num: number | null; aspect_ratio_den: number | null; properties: string[] }
|
||||
@@ -800,6 +806,11 @@ export type VideoProps = { pixel_format: string | null; color_range: string | nu
|
||||
* Represents a physical or virtual storage volume in the system
|
||||
*/
|
||||
export type Volume = {
|
||||
/**
|
||||
* Fingerprint of the volume as a hash of its properties, not persisted to the database
|
||||
* Used as the unique identifier for a volume in this module
|
||||
*/
|
||||
fingerprint: VolumeFingerprint | null;
|
||||
/**
|
||||
* Database ID (None if not yet committed to database)
|
||||
*/
|
||||
@@ -863,11 +874,7 @@ total_bytes_capacity: string;
|
||||
/**
|
||||
* Available storage space in bytes
|
||||
*/
|
||||
total_bytes_available: string;
|
||||
/**
|
||||
* Fingerprint of the volume, not persisted to the database
|
||||
*/
|
||||
fingerprint: string }
|
||||
total_bytes_available: string }
|
||||
|
||||
/**
|
||||
* Events emitted by the Volume Manager when volume state changes
|
||||
@@ -898,4 +905,7 @@ export type VolumeEvent =
|
||||
*/
|
||||
{ VolumeError: { fingerprint: VolumeFingerprint; error: string } }
|
||||
|
||||
/**
|
||||
* A fingerprint of a volume, used to identify it when it is not persisted in the database
|
||||
*/
|
||||
export type VolumeFingerprint = number[]
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"license": "GPL-3.0-only",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"sideEffects": false,
|
||||
"sideEffects": ["./style/index.js", "./style/style.scss"],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./src/forms": "./src/forms/index.ts",
|
||||
|
||||
@@ -7,6 +7,9 @@ function alpha(variableName) {
|
||||
}
|
||||
|
||||
module.exports = function (app, options) {
|
||||
/**
|
||||
* @type {import('tailwindcss').Config}
|
||||
*/
|
||||
let config = {
|
||||
content: [
|
||||
`../../apps/${app}/src/**/*.{ts,tsx,html,stories.tsx}`,
|
||||
@@ -20,8 +23,7 @@ module.exports = function (app, options) {
|
||||
sm: '650px',
|
||||
md: '868px',
|
||||
lg: '1024px',
|
||||
xl: '1280px',
|
||||
...defaultTheme.screens
|
||||
xl: '1280px'
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [...defaultTheme.fontFamily.sans],
|
||||
@@ -175,6 +177,12 @@ module.exports = function (app, options) {
|
||||
]
|
||||
};
|
||||
|
||||
if (app === 'landing') {
|
||||
console.log('CONFIGURING TAILWIND for Landing');
|
||||
config.theme.fontFamily.sans = ['var(--font-inter)', ...defaultTheme.fontFamily.sans];
|
||||
config.theme.fontFamily.plex = ['var(--font-plex-sans)', ...defaultTheme.fontFamily.sans];
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
|
||||
BIN
pnpm-lock.yaml
generated
BIN
pnpm-lock.yaml
generated
Binary file not shown.
163
scripts/switch_servers.ps1
Normal file
163
scripts/switch_servers.ps1
Normal file
@@ -0,0 +1,163 @@
|
||||
# Usage
|
||||
# .\switch_servers.ps1 dev # Will prompt for relay server modification
|
||||
# .\switch_servers.ps1 prod # Will prompt for relay server modification
|
||||
# .\switch_servers.ps1 dev -r # Will automatically modify relay servers
|
||||
# .\switch_servers.ps1 prod -r # Will automatically modify relay servers
|
||||
# .\switch_servers.ps1 dev -s # Will skip relay server modification without prompting
|
||||
# .\switch_servers.ps1 prod -s # Will skip relay server modification without prompting
|
||||
|
||||
|
||||
# File paths
|
||||
$rustFile = "core/crates/cloud-services/src/lib.rs"
|
||||
$tsxFile = "interface/util/index.tsx"
|
||||
$coreFile = "core/src/lib.rs"
|
||||
|
||||
# Function to prompt for relay servers change
|
||||
function Prompt-RelayServers {
|
||||
while ($true) {
|
||||
$response = Read-Host "Do you want to modify relay servers as well? (y/n)"
|
||||
switch ($response.ToLower()) {
|
||||
'y' { return $true }
|
||||
'n' { return $false }
|
||||
default { Write-Host "Please answer y or n." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Check arguments
|
||||
if ($args.Count -lt 1 -or $args.Count -gt 2) {
|
||||
Write-Host "Usage: <script> <dev|prod> [-r|-s]"
|
||||
Write-Host " -r: Automatically modify relay servers without prompting"
|
||||
Write-Host " -s: Skip relay servers modification without prompting"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check environment argument
|
||||
$env = $args[0]
|
||||
if ($env -notmatch '^(dev|prod)$') {
|
||||
Write-Host "Invalid argument. Use 'dev' or 'prod'"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check flags for relay server handling
|
||||
$modifyRelay = $false
|
||||
if ($args.Count -eq 2) {
|
||||
switch ($args[1]) {
|
||||
'-r' { $modifyRelay = $true }
|
||||
'-s' { $modifyRelay = $false }
|
||||
default {
|
||||
Write-Host "Invalid flag. Use -r or -s"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$modifyRelay = Prompt-RelayServers
|
||||
}
|
||||
|
||||
# Function to update file content with regex
|
||||
function Update-FileContent {
|
||||
param (
|
||||
[string]$FilePath,
|
||||
[string]$Pattern,
|
||||
[string]$Replacement
|
||||
)
|
||||
|
||||
$content = Get-Content $FilePath -Raw
|
||||
$content = [regex]::Replace($content, $Pattern, $Replacement)
|
||||
Set-Content -Path $FilePath -Value $content -NoNewline
|
||||
}
|
||||
|
||||
switch ($env) {
|
||||
'dev' {
|
||||
# Update Rust file
|
||||
Update-FileContent $rustFile `
|
||||
'^pub const AUTH_SERVER_URL.*' `
|
||||
'// pub const AUTH_SERVER_URL: &str = "https://auth.spacedrive.com";'
|
||||
Update-FileContent $rustFile `
|
||||
'^// pub const AUTH_SERVER_URL.*localhost.*' `
|
||||
'pub const AUTH_SERVER_URL: &str = "http://localhost:9420";'
|
||||
|
||||
# Update TypeScript file
|
||||
Update-FileContent $tsxFile `
|
||||
"^export const AUTH_SERVER_URL.*" `
|
||||
"// export const AUTH_SERVER_URL = 'https://auth.spacedrive.com';"
|
||||
Update-FileContent $tsxFile `
|
||||
"^// export const AUTH_SERVER_URL.*localhost.*" `
|
||||
"export const AUTH_SERVER_URL = 'http://localhost:9420';"
|
||||
|
||||
if ($modifyRelay) {
|
||||
# Comment out production relay
|
||||
Update-FileContent $coreFile `
|
||||
'^\s*\.unwrap_or_else\(\|_\| "https://relay\.spacedrive\.com:4433/"\.to_string\(\)\)' `
|
||||
'$0 // .unwrap_or_else(|_| "https://relay.spacedrive.com:4433/".to_string())'
|
||||
# Uncomment development relay
|
||||
Update-FileContent $coreFile `
|
||||
'^\s*// \.unwrap_or_else\(\|_\| "http://localhost:8081/"\.to_string\(\)\)' `
|
||||
'.unwrap_or_else(|_| "http://localhost:8081/".to_string())'
|
||||
|
||||
# Comment out production pkarr
|
||||
Update-FileContent $coreFile `
|
||||
'^\s*\.unwrap_or_else\(\|_\| "https://irohdns\.spacedrive\.com/pkarr"\.to_string\(\)\)' `
|
||||
'$0 // .unwrap_or_else(|_| "https://irohdns.spacedrive.com/pkarr".to_string())'
|
||||
# Uncomment development pkarr
|
||||
Update-FileContent $coreFile `
|
||||
'^\s*// \.unwrap_or_else\(\|_\| "http://localhost:8080/pkarr"\.to_string\(\)\)' `
|
||||
'.unwrap_or_else(|_| "http://localhost:8080/pkarr".to_string())'
|
||||
|
||||
# Comment out production cloud domain
|
||||
Update-FileContent $coreFile `
|
||||
'^\s*\.unwrap_or_else\(\|_\| "cloud\.spacedrive\.com"\.to_string\(\)\)' `
|
||||
'$0 // .unwrap_or_else(|_| "cloud.spacedrive.com".to_string())'
|
||||
# Uncomment development cloud domain
|
||||
Update-FileContent $coreFile `
|
||||
'^\s*// \.unwrap_or_else\(\|_\| "localhost"\.to_string\(\)\)' `
|
||||
'.unwrap_or_else(|_| "localhost".to_string())'
|
||||
}
|
||||
}
|
||||
'prod' {
|
||||
# Update Rust file
|
||||
Update-FileContent $rustFile `
|
||||
'^// pub const AUTH_SERVER_URL.*spacedrive.*' `
|
||||
'pub const AUTH_SERVER_URL: &str = "https://auth.spacedrive.com";'
|
||||
Update-FileContent $rustFile `
|
||||
'^pub const AUTH_SERVER_URL.*localhost.*' `
|
||||
'// pub const AUTH_SERVER_URL: &str = "http://localhost:9420";'
|
||||
|
||||
# Update TypeScript file
|
||||
Update-FileContent $tsxFile `
|
||||
"^// export const AUTH_SERVER_URL.*spacedrive.*" `
|
||||
"export const AUTH_SERVER_URL = 'https://auth.spacedrive.com';"
|
||||
Update-FileContent $tsxFile `
|
||||
"^export const AUTH_SERVER_URL.*localhost.*" `
|
||||
"// export const AUTH_SERVER_URL = 'http://localhost:9420';"
|
||||
|
||||
if ($modifyRelay) {
|
||||
# Uncomment production relay
|
||||
Update-FileContent $coreFile `
|
||||
'^\s*// \.unwrap_or_else\(\|_\| "https://relay\.spacedrive\.com:4433/"\.to_string\(\)\)' `
|
||||
'.unwrap_or_else(|_| "https://relay.spacedrive.com:4433/".to_string())'
|
||||
# Comment out development relay
|
||||
Update-FileContent $coreFile `
|
||||
'^\s*\.unwrap_or_else\(\|_\| "http://localhost:8081/"\.to_string\(\)\)' `
|
||||
'// .unwrap_or_else(|_| "http://localhost:8081/".to_string())'
|
||||
|
||||
# Uncomment production pkarr
|
||||
Update-FileContent $coreFile `
|
||||
'^\s*// \.unwrap_or_else\(\|_\| "https://irohdns\.spacedrive\.com/pkarr"\.to_string\(\)\)' `
|
||||
'.unwrap_or_else(|_| "https://irohdns.spacedrive.com/pkarr".to_string())'
|
||||
# Comment out development pkarr
|
||||
Update-FileContent $coreFile `
|
||||
'^\s*\.unwrap_or_else\(\|_\| "http://localhost:8080/pkarr"\.to_string\(\)\)' `
|
||||
'// .unwrap_or_else(|_| "http://localhost:8080/pkarr".to_string())'
|
||||
|
||||
# Uncomment production cloud domain
|
||||
Update-FileContent $coreFile `
|
||||
'^\s*// \.unwrap_or_else\(\|_\| "cloud\.spacedrive\.com"\.to_string\(\)\)' `
|
||||
'.unwrap_or_else(|_| "cloud.spacedrive.com".to_string())'
|
||||
# Comment out development cloud domain
|
||||
Update-FileContent $coreFile `
|
||||
'^\s*\.unwrap_or_else\(\|_\| "localhost"\.to_string\(\)\)' `
|
||||
'// .unwrap_or_else(|_| "localhost".to_string())'
|
||||
}
|
||||
}
|
||||
}
|
||||
113
scripts/switch_servers.sh
Executable file
113
scripts/switch_servers.sh
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/bin/bash
|
||||
|
||||
## Usage:
|
||||
# ./switch_servers.sh dev # Will prompt for relay server modification
|
||||
# ./switch_servers.sh prod # Will prompt for relay server modification
|
||||
# ./switch_servers.sh dev -r # Will automatically modify relay servers
|
||||
# ./switch_servers.sh prod -r # Will automatically modify relay servers
|
||||
# ./switch_servers.sh dev -s # Will skip relay server modification without prompting
|
||||
# ./switch_servers.sh prod -s # Will skip relay server modification without prompting
|
||||
|
||||
|
||||
|
||||
# Create cleanup function
|
||||
cleanup() {
|
||||
rm -f "$rust_file-e" "$tsx_file-e" "$core_file-e"
|
||||
}
|
||||
|
||||
# Set trap for cleanup on script exit
|
||||
trap cleanup EXIT
|
||||
|
||||
rust_file="core/crates/cloud-services/src/lib.rs"
|
||||
tsx_file="interface/util/index.tsx"
|
||||
core_file="core/src/lib.rs"
|
||||
|
||||
# Function to prompt for relay servers change
|
||||
prompt_relay() {
|
||||
while true; do
|
||||
read -p "Do you want to modify relay servers as well? (y/n): " yn
|
||||
case $yn in
|
||||
[Yy]* ) return 0;;
|
||||
[Nn]* ) return 1;;
|
||||
* ) echo "Please answer y or n.";;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
if [ $# -ne 1 ] && [ $# -ne 2 ]; then
|
||||
echo "Usage: $0 <dev|prod> [-r|-s]"
|
||||
echo " -r: Automatically modify relay servers without prompting"
|
||||
echo " -s: Skip relay servers modification without prompting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check flags for relay server handling
|
||||
modify_relay=false
|
||||
if [ "$2" = "-r" ]; then
|
||||
modify_relay=true
|
||||
elif [ "$2" = "-s" ]; then
|
||||
modify_relay=false
|
||||
elif [ $# -eq 1 ]; then
|
||||
prompt_relay && modify_relay=true
|
||||
fi
|
||||
|
||||
case $1 in
|
||||
"dev")
|
||||
# Update Rust file
|
||||
sed -i'' -e 's|^pub const AUTH_SERVER_URL.*|// pub const AUTH_SERVER_URL: \&str = "https:\/\/auth.spacedrive.com";|' "$rust_file"
|
||||
sed -i'' -e 's|^// pub const AUTH_SERVER_URL.*localhost.*|pub const AUTH_SERVER_URL: \&str = "http:\/\/localhost:9420";|' "$rust_file"
|
||||
|
||||
# Update TypeScript file
|
||||
sed -i'' -e "s|^export const AUTH_SERVER_URL.*|// export const AUTH_SERVER_URL = 'https:\/\/auth.spacedrive.com';|" "$tsx_file"
|
||||
sed -i'' -e "s|^// export const AUTH_SERVER_URL.*localhost.*|export const AUTH_SERVER_URL = 'http:\/\/localhost:9420';|" "$tsx_file"
|
||||
|
||||
# Update relay servers if requested
|
||||
if [ "$modify_relay" = true ]; then
|
||||
# Comment out production relay
|
||||
sed -i'' -e 's@^\([[:space:]]*\)\.unwrap_or_else(|_| "https://relay.spacedrive.com:4433/".to_string())@\1// .unwrap_or_else(|_| "https://relay.spacedrive.com:4433/".to_string())@' "$core_file"
|
||||
# Uncomment development relay
|
||||
sed -i'' -e 's@^\([[:space:]]*\)// \.unwrap_or_else(|_| "http://localhost:8081/".to_string())@\1.unwrap_or_else(|_| "http://localhost:8081/".to_string())@' "$core_file"
|
||||
|
||||
# Comment out production pkarr
|
||||
sed -i'' -e 's@^\([[:space:]]*\)\.unwrap_or_else(|_| "https://irohdns.spacedrive.com/pkarr".to_string())@\1// .unwrap_or_else(|_| "https://irohdns.spacedrive.com/pkarr".to_string())@' "$core_file"
|
||||
# Uncomment development pkarr
|
||||
sed -i'' -e 's@^\([[:space:]]*\)// \.unwrap_or_else(|_| "http://localhost:8080/pkarr".to_string())@\1.unwrap_or_else(|_| "http://localhost:8080/pkarr".to_string())@' "$core_file"
|
||||
|
||||
# Comment out production cloud domain
|
||||
sed -i'' -e 's@^\([[:space:]]*\)\.unwrap_or_else(|_| "cloud.spacedrive.com".to_string())@\1// .unwrap_or_else(|_| "cloud.spacedrive.com".to_string())@' "$core_file"
|
||||
# Uncomment development cloud domain
|
||||
sed -i'' -e 's@^\([[:space:]]*\)// \.unwrap_or_else(|_| "localhost".to_string())@\1.unwrap_or_else(|_| "localhost".to_string())@' "$core_file"
|
||||
fi
|
||||
;;
|
||||
"prod")
|
||||
# Update Rust file
|
||||
sed -i'' -e 's|^// pub const AUTH_SERVER_URL.*spacedrive.*|pub const AUTH_SERVER_URL: \&str = "https:\/\/auth.spacedrive.com";|' "$rust_file"
|
||||
sed -i'' -e 's|^pub const AUTH_SERVER_URL.*localhost.*|// pub const AUTH_SERVER_URL: \&str = "http:\/\/localhost:9420";|' "$rust_file"
|
||||
|
||||
# Update TypeScript file
|
||||
sed -i'' -e "s|^// export const AUTH_SERVER_URL.*spacedrive.*|export const AUTH_SERVER_URL = 'https:\/\/auth.spacedrive.com';|" "$tsx_file"
|
||||
sed -i'' -e "s|^export const AUTH_SERVER_URL.*localhost.*|// export const AUTH_SERVER_URL = 'http:\/\/localhost:9420';|" "$tsx_file"
|
||||
|
||||
# Update relay servers if requested
|
||||
if [ "$modify_relay" = true ]; then
|
||||
# Uncomment production relay
|
||||
sed -i'' -e 's@^\([[:space:]]*\)// \.unwrap_or_else(|_| "https://relay.spacedrive.com:4433/".to_string())@\1.unwrap_or_else(|_| "https://relay.spacedrive.com:4433/".to_string())@' "$core_file"
|
||||
# Comment out development relay
|
||||
sed -i'' -e 's@^\([[:space:]]*\)\.unwrap_or_else(|_| "http://localhost:8081/".to_string())@\1// .unwrap_or_else(|_| "http://localhost:8081/".to_string())@' "$core_file"
|
||||
|
||||
# Uncomment production pkarr
|
||||
sed -i'' -e 's@^\([[:space:]]*\)// \.unwrap_or_else(|_| "https://irohdns.spacedrive.com/pkarr".to_string())@\1.unwrap_or_else(|_| "https://irohdns.spacedrive.com/pkarr".to_string())@' "$core_file"
|
||||
# Comment out development pkarr
|
||||
sed -i'' -e 's@^\([[:space:]]*\)\.unwrap_or_else(|_| "http://localhost:8080/pkarr".to_string())@\1// .unwrap_or_else(|_| "http://localhost:8080/pkarr".to_string())@' "$core_file"
|
||||
|
||||
# Uncomment production cloud domain
|
||||
sed -i'' -e 's@^\([[:space:]]*\)// \.unwrap_or_else(|_| "cloud.spacedrive.com".to_string())@\1.unwrap_or_else(|_| "cloud.spacedrive.com".to_string())@' "$core_file"
|
||||
# Comment out development cloud domain
|
||||
sed -i'' -e 's@^\([[:space:]]*\)\.unwrap_or_else(|_| "localhost".to_string())@\1// .unwrap_or_else(|_| "localhost".to_string())@' "$core_file"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Invalid argument. Use 'dev' or 'prod'"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user