Files: wake the task manager when a deletion queues its undelete task #234
No files matched your search
@@ -653,6 +653,8 @@ async fn overwrite_and_delete_item(
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
// inbuxa: the deleted source queued its undelete task
|
||||
server.notify_task_queue();
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
|
||||
}
|
||||
@@ -807,6 +809,9 @@ async fn move_item(
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
// inbuxa: a move to another account deletes the source, which queued
|
||||
// its undelete task
|
||||
server.notify_task_queue();
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
|
||||
}
|
||||
|
||||
@@ -129,6 +129,10 @@ impl DestroyArchive<Vec<u32>> {
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
// inbuxa: deleted files queue the task that notes them for
|
||||
// undelete; wake the task manager rather than leave the task to
|
||||
// its next periodic look at the queue
|
||||
server.notify_task_queue();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -495,6 +495,8 @@ impl FileNodeCopy for Server {
|
||||
.await
|
||||
.and_then(|ids| ids.last_change_id(account_id))
|
||||
.caused_by(trc::location!())?;
|
||||
// inbuxa: replaced files queued their undelete tasks
|
||||
self.notify_task_queue();
|
||||
response.new_state = State::Exact(change_id);
|
||||
}
|
||||
|
||||
|
||||
@@ -764,6 +764,8 @@ impl FileNodeSet for Server {
|
||||
.await
|
||||
.and_then(|ids| ids.last_change_id(account_id))
|
||||
.caused_by(trc::location!())?;
|
||||
// inbuxa: destroyed files queued their undelete tasks
|
||||
self.notify_task_queue();
|
||||
|
||||
response.new_state = State::Exact(change_id).into();
|
||||
}
|
||||
|
||||
@@ -341,6 +341,39 @@ pub async fn wait_for_tasks(server: &Server, skip_not_due: bool, skip_permanent_
|
||||
}
|
||||
}
|
||||
|
||||
/// inbuxa: waits for the task queue to empty, giving up after `within`, and
|
||||
/// returns the task still queued then. The task manager only looks at the
|
||||
/// queue again when woken or every few minutes, so a write that queues a
|
||||
/// task without waking it shows up here as a task that sits.
|
||||
pub async fn task_left_after(server: &Server, within: std::time::Duration) -> Option<Task> {
|
||||
let give_up_at = std::time::Instant::now() + within;
|
||||
loop {
|
||||
let mut pending = None;
|
||||
server
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
ValueKey::from(ValueClass::TaskQueue(TaskQueueClass::Task { id: 0 })),
|
||||
ValueKey::from(ValueClass::TaskQueue(TaskQueueClass::Task { id: u64::MAX })),
|
||||
)
|
||||
.ascending(),
|
||||
|_, value| {
|
||||
pending = Some(Task::deserialize(value)?);
|
||||
Ok(false)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if pending.is_none() || std::time::Instant::now() > give_up_at {
|
||||
return pending;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn assert_is_empty(server: &Server, include_registry: bool) {
|
||||
// Wait for pending index tasks
|
||||
wait_for_tasks(server, false, false).await;
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Coffey Labs LLC
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// inbuxa: deleting a file queues the task that archives it for undelete
|
||||
// (UD-1). Each way of deleting one must wake the task manager, or that task
|
||||
// sits until its next periodic look at the queue, minutes later.
|
||||
|
||||
use crate::utils::{server::TestServer, storage::task_left_after};
|
||||
use hyper::StatusCode;
|
||||
use serde_json::json;
|
||||
use std::time::Duration;
|
||||
|
||||
const BASE: &str = "/dav/file/john%40example.com";
|
||||
|
||||
pub async fn test(test: &TestServer) {
|
||||
println!("Running file deletion task tests...");
|
||||
let account = test.account("[email protected]");
|
||||
let client = account.webdav_client();
|
||||
test.wait_for_tasks().await;
|
||||
|
||||
// WebDAV DELETE
|
||||
let path = format!("{BASE}/tasks-1.txt");
|
||||
put(test, &path).await;
|
||||
client
|
||||
.request("DELETE", &path, "")
|
||||
.await
|
||||
.with_status(StatusCode::NO_CONTENT);
|
||||
assert_tasks_run(test, "WebDAV DELETE").await;
|
||||
|
||||
// WebDAV COPY over an existing file deletes the file it replaces
|
||||
let (from, to) = (format!("{BASE}/tasks-2.txt"), format!("{BASE}/tasks-3.txt"));
|
||||
put(test, &from).await;
|
||||
put(test, &to).await;
|
||||
client
|
||||
.request_with_headers("COPY", &from, [("destination", to.as_str())], "")
|
||||
.await
|
||||
.with_status(StatusCode::NO_CONTENT);
|
||||
assert_tasks_run(test, "WebDAV COPY over a file").await;
|
||||
|
||||
// WebDAV MOVE over an existing file deletes the source
|
||||
client
|
||||
.request_with_headers("MOVE", &from, [("destination", to.as_str())], "")
|
||||
.await
|
||||
.with_status(StatusCode::NO_CONTENT);
|
||||
assert_tasks_run(test, "WebDAV MOVE over a file").await;
|
||||
client
|
||||
.request("DELETE", &to, "")
|
||||
.await
|
||||
.with_status(StatusCode::NO_CONTENT);
|
||||
assert_tasks_run(test, "WebDAV DELETE").await;
|
||||
|
||||
// JMAP FileNode/set destroy
|
||||
let response = account
|
||||
.jmap_method_calls(json!([
|
||||
["Blob/upload", {
|
||||
"accountId": account.id_string(),
|
||||
"create": { "file": { "data": [{ "data:asText": "tasks" }] } }
|
||||
}, "0"],
|
||||
["FileNode/set", {
|
||||
"accountId": account.id_string(),
|
||||
"create": { "i0": { "name": "tasks-4.txt", "parentId": null, "blobId": "#file" } }
|
||||
}, "1"]
|
||||
]))
|
||||
.await
|
||||
.into_inner();
|
||||
let file_id = response
|
||||
.pointer("/methodResponses/1/1/created/i0/id")
|
||||
.and_then(|id| id.as_str())
|
||||
.unwrap_or_else(|| panic!("{response}"))
|
||||
.to_string();
|
||||
let response = account
|
||||
.jmap_method_calls(json!([
|
||||
["FileNode/set", {
|
||||
"accountId": account.id_string(),
|
||||
"destroy": [&file_id]
|
||||
}, "0"]
|
||||
]))
|
||||
.await
|
||||
.into_inner();
|
||||
assert_eq!(
|
||||
response.pointer("/methodResponses/0/1/destroyed/0"),
|
||||
Some(&json!(file_id)),
|
||||
"{response}"
|
||||
);
|
||||
assert_tasks_run(test, "JMAP FileNode/set destroy").await;
|
||||
}
|
||||
|
||||
async fn put(test: &TestServer, path: &str) {
|
||||
test.account("[email protected]")
|
||||
.webdav_client()
|
||||
.request_with_headers("PUT", path, [("content-type", "text/plain")], "tasks")
|
||||
.await
|
||||
.with_status(StatusCode::CREATED);
|
||||
}
|
||||
|
||||
async fn assert_tasks_run(test: &TestServer, what: &str) {
|
||||
if let Some(task) = task_left_after(&test.server, Duration::from_secs(10)).await {
|
||||
panic!("{what}: a task was still queued after 10 s: {task:?}");
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*
|
||||
* Modified by Coffey Labs in 2026 for INBUXA.
|
||||
*/
|
||||
|
||||
use crate::utils::server::TestServerBuilder;
|
||||
@@ -32,6 +34,7 @@ pub mod cal_query;
|
||||
pub mod cal_scheduling;
|
||||
pub mod card_query;
|
||||
pub mod copy_move;
|
||||
pub mod file_tasks; // inbuxa: file deletes wake the task manager
|
||||
pub mod lock;
|
||||
pub mod mkcol;
|
||||
pub mod multiget;
|
||||
@@ -183,6 +186,7 @@ pub async fn webdav_tests() {
|
||||
}
|
||||
basic::test(&test).await;
|
||||
put_get::test(&test).await;
|
||||
file_tasks::test(&test).await;
|
||||
mkcol::test(&test).await;
|
||||
copy_move::test(&test, assisted_discovery).await;
|
||||
prop::test(&test, assisted_discovery).await;
|
||||
|
||||
Reference in new issue
Block a user