Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
46a36af
Add baseline Linux sync support
Mike-Solar Jun 5, 2026
796cfa5
Add KDE Linux sync backend integration
Mike-Solar Jun 5, 2026
930c31e
Revert "Add KDE Linux sync backend integration"
Mike-Solar Jun 5, 2026
696f40d
Document Linux full sync strategy
Mike-Solar Jun 5, 2026
7e08003
Add Linux autostart and packaging support
Mike-Solar Jun 5, 2026
db8e4c9
Exclude Windows notification crate from Linux workspace
Mike-Solar Jun 5, 2026
4d4c33f
Add Linux notifications and packaging workflows
Mike-Solar Jun 5, 2026
c456109
Add macOS full sync notifications and autostart
Mike-Solar Jun 5, 2026
b7e3338
Add non-Windows sync support
Mike-Solar Jun 5, 2026
a599db8
Fix Linux panic due to WebKit environment variable; fix Arch PKGBUILD
Mike-Solar Jun 5, 2026
d40d623
Fix Arch package release build
Mike-Solar Jun 5, 2026
9766b79
Improve popup conflict resolution entry
Mike-Solar Jun 6, 2026
9e1d4e0
Support syncing existing local folders
Mike-Solar Jun 6, 2026
0a9cbf3
Fix tauri version mismatch
Mike-Solar Jun 7, 2026
2065ccf
Fix compilation errors
Mike-Solar Jun 7, 2026
10cbc2d
fix(macos): add macOS support for deep links and background sync
Mike-Solar Jun 7, 2026
09f31cb
fix linux compilation error
Mike-Solar Jun 7, 2026
19d3be2
fix: improve conflict resolution and remove incorrect OAuth refresh flow
Mike-Solar Jun 13, 2026
120f0b7
Fix token refresh racing
Mike-Solar Jun 18, 2026
b829e4a
Fix token expiring
Mike-Solar Jun 18, 2026
cc69491
Hide macOS Dock icon.
Mike-Solar Jun 29, 2026
6ce35ef
Add close button
Mike-Solar Jun 29, 2026
e020476
Fix macOS Dock icon still shows after close window
Mike-Solar Jun 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: CI

on:
push:
branches:
- main
pull_request:
branches:
- main

permissions:
contents: read

jobs:
linux-tests:
name: Linux tests
runs-on: ubuntu-22.04

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential \
curl \
file \
libayatana-appindicator3-dev \
libssl-dev \
libwebkit2gtk-4.1-dev \
librsvg2-dev \
pkg-config

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Cache Cargo
uses: Swatinem/rust-cache@v2

- name: Check Linux workspace
run: cargo check --workspace

- name: Run Linux tests
run: |
cargo test -p cloudreve-desktop linux_autostart_tests
cargo test -p cloudreve-sync drive::placeholder::tests
80 changes: 80 additions & 0 deletions .github/workflows/release-linux.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Release Linux Packages

on:
workflow_dispatch:
push:
tags:
- 'v*'

permissions:
contents: write

jobs:
linux-packages:
name: Build deb and rpm
runs-on: ubuntu-22.04

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Linux packaging dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential \
curl \
file \
libayatana-appindicator3-dev \
libssl-dev \
libwebkit2gtk-4.1-dev \
librsvg2-dev \
patchelf \
pkg-config \
rpm

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Cache Cargo
uses: Swatinem/rust-cache@v2

- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Install Yarn
run: npm install -g yarn@1.22.22

- name: Install frontend dependencies
working-directory: ui
run: yarn install --frozen-lockfile

- name: Build frontend
working-directory: ui
run: yarn run build

- name: Install Tauri CLI
run: cargo install tauri-cli --version '^2' --locked

- name: Build Linux packages
working-directory: src-tauri
run: cargo tauri build --bundles deb,rpm --config '{"build":{"beforeBuildCommand":""}}'

- name: Upload Linux packages
uses: actions/upload-artifact@v4
with:
name: cloudreve-linux-packages
path: |
src-tauri/target/release/bundle/deb/*.deb
src-tauri/target/release/bundle/rpm/*.rpm
if-no-files-found: error

- name: Attach packages to GitHub Release
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
files: |
src-tauri/target/release/bundle/deb/*.deb
src-tauri/target/release/bundle/rpm/*.rpm
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ Cargo.lock
/release/
/debug/
dist/
package/*.exe
package/*.exe

# Package manager files not used by the current Yarn-based build
ui/pnpm-lock.yaml
ui/pnpm-workspace.yaml
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ resolver = "2"
members = [
"crates/cloudreve-api",
"crates/cloudreve-sync",
"crates/win32_notif",
"src-tauri",
]
exclude = [
"crates/win32_notif",
]
73 changes: 51 additions & 22 deletions crates/cloudreve-api/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use serde::Serialize;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::sync::{Mutex, RwLock};

const API_PREFIX: &str = "/api/v4";
pub const CR_HEADER_PREFIX: &str = "X-Cr-";
Expand Down Expand Up @@ -143,6 +143,7 @@ pub struct Client {
pub(crate) config: ClientConfig,
pub(crate) http_client: HttpClient,
pub(crate) tokens: Arc<RwLock<TokenStore>>,
refresh_lock: Arc<Mutex<()>>,
pub(crate) purchase_ticket: Arc<RwLock<Option<String>>>,
on_credential_refreshed: Option<OnCredentialRefreshed>,
on_credential_invalid: Option<OnCredentialInvalid>,
Expand All @@ -164,6 +165,7 @@ impl Client {
config,
http_client,
tokens: Arc::new(RwLock::new(TokenStore::new())),
refresh_lock: Arc::new(Mutex::new(())),
purchase_ticket: Arc::new(RwLock::new(None)),
on_credential_refreshed: None,
on_credential_invalid: None,
Expand Down Expand Up @@ -246,13 +248,15 @@ impl Client {
store.access_token = Some(token.access_token.clone());
store.refresh_token = Some(token.refresh_token.clone());

// Parse RFC3339 timestamps
if let Ok(exp) = DateTime::parse_from_rfc3339(&token.access_expires) {
store.access_token_expires = Some(exp.with_timezone(&Utc));
}
if let Ok(exp) = DateTime::parse_from_rfc3339(&token.refresh_expires) {
store.refresh_token_expires = Some(exp.with_timezone(&Utc));
}
let access_expires = DateTime::parse_from_rfc3339(&token.access_expires)
.map_err(|e| ApiError::InvalidToken(format!("Invalid access expiry: {}", e)))?
.with_timezone(&Utc);
let refresh_expires = DateTime::parse_from_rfc3339(&token.refresh_expires)
.map_err(|e| ApiError::InvalidToken(format!("Invalid refresh expiry: {}", e)))?
.with_timezone(&Utc);

store.access_token_expires = Some(access_expires);
store.refresh_token_expires = Some(refresh_expires);

Ok(())
}
Expand Down Expand Up @@ -340,13 +344,34 @@ impl Client {
// Access token expired, need to refresh
drop(store); // Release read lock before calling refresh

self.refresh_access_token().await
self.refresh_access_token(false).await
}

/// Refresh the access token using the refresh token
async fn refresh_access_token(&self) -> ApiResult<String> {
/// Refresh the access token using the refresh token.
/// Cloudreve V4 uses `/session/token/refresh` for all refresh tokens,
/// including tokens originally obtained via OAuth. The OAuth token endpoint
/// `/session/oauth/token` only supports `authorization_code` grant and is
/// not used here.
async fn refresh_access_token(&self, force: bool) -> ApiResult<String> {
let _guard = self.refresh_lock.lock().await;

let refresh_token = {
let store = self.tokens.read().await;

if !store.has_tokens() {
self.notify_credential_invalid().await;
return Err(ApiError::NoTokensAvailable);
}

if store.is_refresh_token_expired() {
self.notify_credential_invalid().await;
return Err(ApiError::RefreshTokenExpired);
}

if !force && !store.is_access_token_expired() {
return Ok(store.access_token.clone().unwrap());
}

store
.refresh_token
.clone()
Expand All @@ -357,7 +382,13 @@ impl Client {
let url = self.build_url("/session/token/refresh");
let request = RefreshTokenRequest { refresh_token };

let response = self.http_client.post(&url).json(&request).send().await?;
let response = self
.http_client
.post(&url)
.timeout(std::time::Duration::from_secs(self.config.timeout_seconds))
.json(&request)
.send()
.await?;

let api_response: ApiResponse<Token> = response.json().await?;

Expand Down Expand Up @@ -403,7 +434,10 @@ impl Client {
R: DeserializeOwned + Default,
{
let url = self.build_url(path);
let mut request = self.http_client.request(method, &url);
let mut request = self
.http_client
.request(method, &url)
.timeout(std::time::Duration::from_secs(self.config.timeout_seconds));

// Add authentication header if needed
if !options.no_credential {
Expand Down Expand Up @@ -462,12 +496,6 @@ impl Client {

// Check response code
if api_response.code != ErrorCode::Success as i32 {
// Check if this is a credential error and invoke callback
if let Some(error_code) = ErrorCode::from_code(api_response.code) {
if error_code.is_credential_error() {
self.notify_credential_invalid().await;
}
}
return Err(ApiError::from_response(api_response));
}

Expand All @@ -492,9 +520,10 @@ impl Client {
.await
{
Ok(result) => Ok(result),
Err(ApiError::AccessTokenExpired) => {
// Token expired, refresh and retry
self.refresh_access_token().await?;
Err(e) if e.is_token_expired() || e.requires_login() => {
// Server-side token state can be ahead of the local expiry timestamp.
// Force a refresh once before treating the credentials as invalid.
self.refresh_access_token(true).await?;
self.send_internal(path, method, body, options).await
}
Err(e) => Err(e),
Expand Down
17 changes: 12 additions & 5 deletions crates/cloudreve-sync/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ tracing-appender = "0.2"
tower-http = { version = "0.5", features = ["trace", "cors"] }
futures = "0.3"
reqwest = { version = "0.12", features = ["json", "stream", "multipart"] }
md-5 = "0.10"
sha1 = "0.10"
sha2 = "0.10"
image = "0.24"
url = "2.5"
cloudreve-api = { path = "../cloudreve-api" }
windows-core = "0.58.0"
nt-time = "0.8.0"
widestring = "1.0.2"
flagset = "0.4.5"
Expand All @@ -43,9 +44,18 @@ aes = "0.8"
ctr = "0.9"
tokio-util = { version = "0.7", features = ["io"] }
globset = "0.4"

[target.'cfg(windows)'.dependencies]
windows-core = "0.58.0"
win32_notif = { path = "../win32_notif" }

[dependencies.windows]
[target.'cfg(target_os = "linux")'.dependencies]
notify-rust = "4.11"

[target.'cfg(target_os = "macos")'.dependencies]
mac-notification-sys = "0.6"

[target.'cfg(windows)'.dependencies.windows]
version = "0.58.0"
features = [
"implement",
Expand Down Expand Up @@ -79,9 +89,6 @@ features = [
"Win32_UI_Notifications",
]

[build-dependencies]
windows = { version = "0.58.0", features = ["Win32_Foundation"] }

[dependencies.uuid]
version = "1.6"
features = ["v4", "serde"]
Expand Down
Loading