Add path change to server

This commit is contained in:
Andras Schmelczer 2026-04-21 20:09:36 +01:00
parent 9183f30b5d
commit dca59a18dc
9 changed files with 225 additions and 29 deletions

View file

@ -11,7 +11,10 @@ use super::{device_id_header::DeviceIdHeader, requests::CreateDocumentVersion};
use crate::{
app_state::{
AppState,
database::models::{StoredDocumentVersion, VaultId},
database::{
InsertBroadcast,
models::{StoredDocumentVersion, VaultId},
},
},
config::user_config::User,
errors::{SyncServerError, client_error, server_error, write_transaction_error},
@ -116,6 +119,8 @@ pub async fn create_document(
);
}
let path_changed = deduped_path != sanitized_relative_path;
let new_version = StoredDocumentVersion {
vault_update_id: last_update_id + 1,
document_id,
@ -130,7 +135,17 @@ pub async fn create_document(
state
.database
.insert_document_version(&vault_id, &new_version, transaction)
.insert_document_version(
&vault_id,
&new_version,
transaction,
InsertBroadcast {
// A brand-new document is always a content change for peers.
content_changed: true,
// Origin needs to know if the server deduped its requested path.
path_changed,
},
)
.await
.map_err(server_error)?;

View file

@ -11,8 +11,9 @@ use super::{device_id_header::DeviceIdHeader, requests::DeleteDocumentVersion};
use crate::{
app_state::{
AppState,
database::models::{
DocumentId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId,
database::{
InsertBroadcast,
models::{DocumentId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId},
},
},
config::user_config::User,
@ -91,7 +92,17 @@ pub async fn delete_document(
state
.database
.insert_document_version(&vault_id, &new_version, transaction)
.insert_document_version(
&vault_id,
&new_version,
transaction,
InsertBroadcast {
// Deletion is a content change peers must learn about.
content_changed: true,
// Delete never renames.
path_changed: false,
},
)
.await
.map_err(server_error)?;

View file

@ -3,7 +3,8 @@ use serde::{self, Serialize};
use ts_rs::TS;
use crate::app_state::database::models::{
DocumentVersion, DocumentVersionWithoutContent, VaultUpdateId,
DocumentUpdateMergedContent, DocumentUpdateMetadata, DocumentVersionWithoutContent,
VaultUpdateId,
};
/// Response to a ping request.
@ -66,7 +67,7 @@ pub struct ListVaultsResponse {
pub user_name: String,
}
/// Response to an update document request.
/// Response to a create/update document request.
#[derive(TS, Debug, Clone, Serialize)]
#[serde(tag = "type")]
#[ts(export)]
@ -74,9 +75,9 @@ pub enum DocumentUpdateResponse {
/// Returned when the created/updated document's content is the same as was
/// sent in the create/update request and thus the response doesn't contain
/// the content because the client must already have it.
FastForwardUpdate(DocumentVersionWithoutContent),
FastForwardUpdate(DocumentUpdateMetadata),
/// Returned when the created/updated document's content is different from
/// what was sent in the create/update request.
MergingUpdate(DocumentVersion),
MergingUpdate(DocumentUpdateMergedContent),
}

View file

@ -11,9 +11,12 @@ use super::device_id_header::DeviceIdHeader;
use crate::{
app_state::{
AppState,
database::models::{
DocumentId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId,
VaultUpdateId,
database::{
InsertBroadcast,
models::{
DocumentId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId,
VaultUpdateId,
},
},
},
config::user_config::User,
@ -120,6 +123,14 @@ pub async fn restore_document_version(
.await
.map_err(server_error)?;
// The current latest (pre-restore) is our baseline for deciding
// whether content and/or path actually change.
let current_latest = state
.database
.get_latest_document(&vault_id, &document_id, Some(&mut *transaction))
.await
.map_err(server_error)?;
let new_version = StoredDocumentVersion {
vault_update_id: last_update_id + 1,
document_id,
@ -132,9 +143,27 @@ pub async fn restore_document_version(
has_been_merged: false,
};
let (content_changed, path_changed) = match &current_latest {
Some(prev) => (
prev.content != new_version.content || prev.is_deleted,
prev.relative_path != new_version.relative_path,
),
// No prior version (shouldn't happen in practice — target_version
// already proved the document exists — but treat defensively).
None => (true, true),
};
state
.database
.insert_document_version(&vault_id, &new_version, transaction)
.insert_document_version(
&vault_id,
&new_version,
transaction,
InsertBroadcast {
content_changed,
path_changed,
},
)
.await
.map_err(server_error)?;

View file

@ -17,7 +17,7 @@ use crate::{
app_state::{
AppState,
database::{
WriteTransaction,
InsertBroadcast, WriteTransaction,
models::{DocumentId, StoredDocumentVersion, VaultId, VaultUpdateId},
},
},
@ -292,6 +292,14 @@ pub async fn update_document(
latest_version.relative_path.clone()
};
let content_changed = merged_content != latest_version.content;
// Stored path differs from either the prior stored path (peers need
// to learn about the rename) or from the path the origin sent
// (origin needs to learn if its rename was deduped or rejected by
// first-rename-wins).
let path_changed = new_relative_path != latest_version.relative_path
|| new_relative_path != sanitized_relative_path;
let new_version = StoredDocumentVersion {
document_id,
vault_update_id: last_update_id + 1,
@ -306,7 +314,15 @@ pub async fn update_document(
state
.database
.insert_document_version(&vault_id, &new_version, transaction)
.insert_document_version(
&vault_id,
&new_version,
transaction,
InsertBroadcast {
content_changed,
path_changed,
},
)
.await
.map_err(server_error)?;

View file

@ -179,7 +179,8 @@ async fn websocket(
.filter(|client| client.device_id != device_id)
.collect(),
}),
WebSocketServerMessage::VaultUpdate(_) => update.message,
WebSocketServerMessage::VaultUpdate(_)
| WebSocketServerMessage::PathChange(_) => update.message,
};
send_update_over_websocket(&message, &mut sender).await?;