diff --git a/Cargo.lock b/Cargo.lock
index c530fc8..28705c8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2174,11 +2174,15 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
name = "nextcloud_client"
version = "0.1.0"
dependencies = [
+ "bytes",
+ "futures-util",
+ "pin-project-lite",
"reqwest 0.12.28",
"serde",
"serde_json",
"thiserror 2.0.20",
"tokio",
+ "tokio-util",
"url",
]
diff --git a/Tasks.md b/Tasks.md
index a1de89f..c6ddbf4 100644
--- a/Tasks.md
+++ b/Tasks.md
@@ -71,7 +71,7 @@ This document defines the complete project roadmap and task tracking system for
|---|---|---|---|
| [NUT-001](#nut-001) | Establish Repository Structure | Fixed | Foundation |
| [NUT-002](#nut-002) | Implement Shared Rust Backend Library | Fixed | Foundation |
-| [NUT-003](#nut-003) | Implement WebDAV Upload Logic | Triage | Feature |
+| [NUT-003](#nut-003) | Implement WebDAV Upload Logic | Fixed | Feature |
| [NUT-004](#nut-004) | Implement OCS Share Link Generation | Triage | Feature |
| [NUT-005](#nut-005) | Implement Direct Download URL Builder | Triage | Feature |
| [NUT-006](#nut-006) | Implement Credential Storage System | Triage | Feature |
@@ -136,20 +136,20 @@ Implement the shared Rust library that provides all core functionality: WebDAV u
- NUT-001
-
+
### Implement WebDAV Upload Logic
**ID:** NUT-003
-**Status:** Triage
+**Status:** Fixed
**Type:** Feature
**Description:**
Implement file upload using Nextcloud’s WebDAV API. Support streaming uploads, file size detection, and progress callbacks.
**Requirements:**
-- [ ] Implement PUT request to WebDAV endpoint
-- [ ] Support streaming from file or stdin
-- [ ] Provide progress callback API
-- [ ] Handle authentication
+- [x] Implement PUT request to WebDAV endpoint
+- [x] Support streaming from file or stdin
+- [x] Provide progress callback API
+- [x] Handle authentication
**Dependencies:**
- NUT-002
diff --git a/nextcloud_client/Cargo.toml b/nextcloud_client/Cargo.toml
index ccf391d..212952a 100644
--- a/nextcloud_client/Cargo.toml
+++ b/nextcloud_client/Cargo.toml
@@ -6,6 +6,10 @@ edition = "2024"
[dependencies]
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "default-tls"] }
tokio = { version = "1", features = ["sync", "fs", "io-util", "macros", "time"] }
+tokio-util = { version = "0.7", features = ["io"] }
+futures-util = { version = "0.3", default-features = false }
+pin-project-lite = "0.2"
+bytes = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "2.0"
diff --git a/nextcloud_client/src/client.rs b/nextcloud_client/src/client.rs
index e8c6923..4cf645a 100644
--- a/nextcloud_client/src/client.rs
+++ b/nextcloud_client/src/client.rs
@@ -24,8 +24,8 @@ pub struct ServerStatus {
/// The core Nextcloud client instance.
#[derive(Debug, Clone)]
pub struct NextcloudClient {
- config: ClientConfig,
- http: reqwest::Client,
+ pub(crate) config: ClientConfig,
+ pub(crate) http: reqwest::Client,
}
impl NextcloudClient {
diff --git a/nextcloud_client/src/lib.rs b/nextcloud_client/src/lib.rs
index 44315c1..017baf1 100644
--- a/nextcloud_client/src/lib.rs
+++ b/nextcloud_client/src/lib.rs
@@ -7,9 +7,12 @@ pub mod client;
pub mod config;
pub mod error;
pub mod models;
+pub mod progress;
+pub mod webdav;
// Convenient top-level re-exports
pub use client::{NextcloudClient, ServerStatus};
pub use config::{AccountCredentials, ClientConfig};
pub use error::{NextcloudError, Result};
pub use models::{UploadOptions, UploadResult};
+pub use progress::{ProgressCallback, ProgressEvent};
diff --git a/nextcloud_client/src/progress.rs b/nextcloud_client/src/progress.rs
new file mode 100644
index 0000000..b2182f8
--- /dev/null
+++ b/nextcloud_client/src/progress.rs
@@ -0,0 +1,152 @@
+use bytes::Bytes;
+use futures_util::Stream;
+use pin_project_lite::pin_project;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::{Context, Poll};
+
+/// Event fired during streaming file uploads to report transfer progress.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ProgressEvent {
+ /// Transfer has started. `total_bytes` is `None` if streaming from an unsized pipe / stdin.
+ Started { total_bytes: Option },
+
+ /// Transfer progress update with running count of transferred bytes.
+ Progress {
+ bytes_transferred: u64,
+ total_bytes: Option,
+ },
+
+ /// Transfer has finished successfully.
+ Completed { total_bytes: u64 },
+}
+
+/// A thread-safe callback invoked when transfer progress occurs.
+pub type ProgressCallback = Arc;
+
+pin_project! {
+ /// A Stream wrapper that tracks transferred bytes and fires progress callbacks.
+ pub struct ProgressStream {
+ #[pin]
+ inner: S,
+ total_bytes: Option,
+ bytes_transferred: u64,
+ callback: Option,
+ started: bool,
+ completed: bool,
+ }
+}
+
+impl ProgressStream {
+ pub fn new(
+ inner: S,
+ total_bytes: Option,
+ callback: Option,
+ ) -> Self {
+ Self {
+ inner,
+ total_bytes,
+ bytes_transferred: 0,
+ callback,
+ started: false,
+ completed: false,
+ }
+ }
+}
+
+impl Stream for ProgressStream
+where
+ S: Stream- >,
+{
+ type Item = std::result::Result;
+
+ fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll