Undelete: deleted files, calendar events, contacts and Sieve scripts are kept and restored where they were (UD-1, UD-8 to UD-11)

Files, events and contacts are noted when deleted for good and archived by
the unindex task when retention is on; Sieve scripts are archived at
deletion. A restore goes back to its folder, calendar or address book if it
still exists, takes a free " (restored)" name, comes back inactive for
scripts, and is refused over quota with the item left archived.
Acceptance tests 6, 8 and 12.
This commit is contained in:
2026-09-18 21:07:27 -07:00
parent 4a631bd0b5
commit 5b963b06c6
16 changed files with 946 additions and 1 deletions
+1
View File
@@ -11,6 +11,7 @@ types = { path = "../types" }
trc = { path = "../trc" }
nlp = { path = "../nlp" }
registry = { path = "../registry" }
inbuxa-features = { path = "../features" }
calcard = { version = "0.3", features = ["rkyv"] }
hashify = "0.2"
rkyv = { version = "0.8.18", features = ["little_endian"] }
+9
View File
@@ -462,6 +462,15 @@ impl DestroyArchive<Archive<&ArchivedCalendarEvent>> {
batch: &mut BatchBuilder,
) -> trc::Result<()> {
let event = self.0;
// inbuxa: UD-1: noted for undelete
crate::inbuxa::note_event(
batch,
account_id,
document_id,
&event
.deserialize::<CalendarEvent>()
.caused_by(trc::location!())?,
)?;
// Delete event
batch
.with_account_id(account_id)
+19
View File
@@ -234,6 +234,15 @@ impl DestroyArchive<Archive<&ArchivedContactCard>> {
)
.caused_by(trc::location!())?;
} else {
// inbuxa: UD-1: noted for undelete
crate::inbuxa::note_card(
batch,
account_id,
document_id,
&card
.deserialize::<ContactCard>()
.caused_by(trc::location!())?,
)?;
// Delete card
batch
.with_document(document_id)
@@ -262,6 +271,16 @@ impl DestroyArchive<Archive<&ArchivedContactCard>> {
document_id: u32,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
// inbuxa: UD-1: noted for undelete
crate::inbuxa::note_card(
batch,
account_id,
document_id,
&self
.0
.deserialize::<ContactCard>()
.caused_by(trc::location!())?,
)?;
batch
.with_account_id(account_id)
.with_collection(Collection::ContactCard)
+18
View File
@@ -82,6 +82,14 @@ impl DestroyArchive<Archive<&ArchivedFileNode>> {
batch: &mut BatchBuilder,
path: String,
) -> trc::Result<()> {
// inbuxa: UD-1: noted for undelete
crate::inbuxa::note_file(
batch,
account_id,
document_id,
&self.0.deserialize::<FileNode>().caused_by(trc::location!())?,
)?;
// Prepare write batch
batch
.with_account_id(account_id)
@@ -143,6 +151,16 @@ impl DestroyArchive<Vec<u32>> {
))
.await?
{
// inbuxa: UD-1: noted for undelete
crate::inbuxa::note_file(
batch,
account_id,
document_id,
&node
.deserialize::<FileNode>()
.caused_by(trc::location!())?,
)?;
// Delete record
batch
.with_document(document_id)
+108
View File
@@ -0,0 +1,108 @@
/*
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
//! Undelete notes for files, calendar events and contacts deleted for good
//! (`docs/spec/features/undelete.md`, UD-1). The rules are in
//! `inbuxa_features::undelete`.
use crate::{calendar::CalendarEvent, contact::ContactCard, file::FileNode};
use inbuxa_features::undelete::{
data::Extra,
groupware::{Kind, Note, note},
};
use registry::schema::{
enums::IndexDocumentType,
structs::{Task, TaskIndexDocument, TaskStatus},
};
use store::write::BatchBuilder;
/// A file (not a folder) deleted for good.
pub fn note_file(
batch: &mut BatchBuilder,
account_id: u32,
document_id: u32,
node: &FileNode,
) -> trc::Result<()> {
let Some(file) = &node.file else {
return Ok(());
};
// Files aren't search-indexed, so nothing else schedules the task that
// archives the note: schedule it here
batch.schedule_task(Task::UnindexDocument(TaskIndexDocument {
account_id: account_id.into(),
document_id: document_id.into(),
document_type: IndexDocumentType::File,
status: TaskStatus::now(),
}));
note(
batch,
Kind::File,
account_id,
document_id,
Note {
deleted_at: 0,
created_at: node.created,
content: None,
blob_hash: Some(file.blob_hash.as_slice().to_vec()),
extra: Extra::FileNode {
parent_id: Some(node.parent_id),
name: node.name.clone(),
media_type: file.media_type.clone(),
size: file.size,
},
},
)
}
/// A calendar event deleted for good.
pub fn note_event(
batch: &mut BatchBuilder,
account_id: u32,
document_id: u32,
event: &CalendarEvent,
) -> trc::Result<()> {
note(
batch,
Kind::CalendarEvent,
account_id,
document_id,
Note {
deleted_at: 0,
created_at: event.created,
content: Some(event.data.event.to_string()),
blob_hash: None,
extra: Extra::CalendarEvent {
calendar_ids: event.names.iter().map(|n| n.parent_id).collect(),
name: event.names.first().map(|n| n.name.clone()).unwrap_or_default(),
},
},
)
}
/// A contact deleted for good.
pub fn note_card(
batch: &mut BatchBuilder,
account_id: u32,
document_id: u32,
card: &ContactCard,
) -> trc::Result<()> {
note(
batch,
Kind::ContactCard,
account_id,
document_id,
Note {
deleted_at: 0,
created_at: card.created,
content: Some(card.card.to_string()),
blob_hash: None,
extra: Extra::ContactCard {
address_book_ids: card.names.iter().map(|n| n.parent_id).collect(),
name: card.names.first().map(|n| n.name.clone()).unwrap_or_default(),
},
},
)
}
+1
View File
@@ -16,6 +16,7 @@ pub mod cache;
pub mod calendar;
pub mod contact;
pub mod file;
pub mod inbuxa; // inbuxa: undelete notes
pub mod scheduling;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]