refactor(file|folder): separate etag and blob_hash
This commit is contained in:
@@ -253,8 +253,10 @@ pub async fn list_favorites_resources(
|
||||
};
|
||||
|
||||
if row.resource_type == "folder" {
|
||||
let resource_id = row.resource_id.to_string();
|
||||
let dto = FolderDto {
|
||||
id: row.resource_id.to_string(),
|
||||
etag: resource_id.clone(),
|
||||
id: resource_id,
|
||||
name: row.name.clone(),
|
||||
path,
|
||||
parent_id: row.parent_id.map(|u| u.to_string()),
|
||||
@@ -277,6 +279,12 @@ pub async fn list_favorites_resources(
|
||||
.as_deref()
|
||||
.unwrap_or("application/octet-stream");
|
||||
let size_bytes = row.size.max(0) as u64;
|
||||
// The favorites list query doesn't select
|
||||
// `blob_hash` — favorites UI displays metadata
|
||||
// only and doesn't trigger ETag-conditional
|
||||
// requests against these rows. If a caller
|
||||
// ever needs the content hash here, widen the
|
||||
// favorites SQL.
|
||||
let dto = FileDto {
|
||||
id: row.resource_id.to_string(),
|
||||
name: row.name.clone(),
|
||||
@@ -294,6 +302,7 @@ pub async fn list_favorites_resources(
|
||||
size_formatted: format_file_size(size_bytes),
|
||||
owner_id: Some(row.owner_id.to_string()),
|
||||
sort_date: None,
|
||||
content_hash: String::new(),
|
||||
etag: String::new(),
|
||||
};
|
||||
FavoritesResourceItemDto {
|
||||
|
||||
@@ -692,8 +692,10 @@ pub async fn list_folder_resources(
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
if row.resource_type == "folder" {
|
||||
let resource_id = row.id.to_string();
|
||||
let dto = FolderDto {
|
||||
id: row.id.to_string(),
|
||||
etag: resource_id.clone(),
|
||||
id: resource_id,
|
||||
name: row.name.clone(),
|
||||
path: String::new(), // cleared — share recipients must not see hierarchy
|
||||
parent_id: row.parent_id.map(|u| u.to_string()),
|
||||
@@ -715,6 +717,14 @@ pub async fn list_folder_resources(
|
||||
.as_deref()
|
||||
.unwrap_or("application/octet-stream");
|
||||
let size_bytes = row.size.max(0) as u64;
|
||||
// `FolderResourceRow` (the UNION ALL row used
|
||||
// by this listing) doesn't carry `blob_hash`,
|
||||
// so neither `content_hash` nor `etag` can be
|
||||
// populated here without widening the SQL. The
|
||||
// REST file-listing UI doesn't issue
|
||||
// conditional requests against these rows —
|
||||
// file download / WebDAV PROPFIND go through
|
||||
// paths that DO carry the hash.
|
||||
let dto = FileDto {
|
||||
id: row.id.to_string(),
|
||||
name: row.name.clone(),
|
||||
@@ -730,6 +740,7 @@ pub async fn list_folder_resources(
|
||||
size_formatted: format_file_size(size_bytes),
|
||||
owner_id: Some(row.owner_id.to_string()),
|
||||
sort_date: None,
|
||||
content_hash: String::new(),
|
||||
etag: String::new(),
|
||||
};
|
||||
FolderResourceItemDto {
|
||||
|
||||
@@ -283,8 +283,10 @@ pub async fn list_recent_resources(
|
||||
};
|
||||
|
||||
if row.resource_type == "folder" {
|
||||
let resource_id = row.resource_id.to_string();
|
||||
let dto = FolderDto {
|
||||
id: row.resource_id.to_string(),
|
||||
etag: resource_id.clone(),
|
||||
id: resource_id,
|
||||
name: row.name.clone(),
|
||||
path,
|
||||
parent_id: row.parent_id.map(|u| u.to_string()),
|
||||
@@ -307,6 +309,8 @@ pub async fn list_recent_resources(
|
||||
.as_deref()
|
||||
.unwrap_or("application/octet-stream");
|
||||
let size_bytes = row.size.max(0) as u64;
|
||||
// Recents listing row doesn't carry blob_hash
|
||||
// (same reason as folder_handler / favorites).
|
||||
let dto = FileDto {
|
||||
id: row.resource_id.to_string(),
|
||||
name: row.name.clone(),
|
||||
@@ -324,6 +328,7 @@ pub async fn list_recent_resources(
|
||||
size_formatted: format_file_size(size_bytes),
|
||||
owner_id: Some(row.owner_id.to_string()),
|
||||
sort_date: None,
|
||||
content_hash: String::new(),
|
||||
etag: String::new(),
|
||||
};
|
||||
RecentResourceItemDto {
|
||||
|
||||
@@ -353,6 +353,7 @@ async fn handle_propfind(
|
||||
// Root folder
|
||||
let root_folder = FolderDto {
|
||||
id: "root".to_string(),
|
||||
etag: "root".to_string(),
|
||||
name: "".to_string(),
|
||||
path: "".to_string(),
|
||||
parent_id: None,
|
||||
@@ -706,7 +707,7 @@ async fn handle_get(
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, &*file.mime_type)
|
||||
.header(header::CONTENT_LENGTH, file.size)
|
||||
.header(header::ETAG, format!("\"{}\"", file.id))
|
||||
.header(header::ETAG, format!("\"{}\"", file.etag))
|
||||
.header(
|
||||
header::LAST_MODIFIED,
|
||||
chrono::DateTime::<Utc>::from_timestamp(file.created_at as i64, 0)
|
||||
@@ -747,7 +748,7 @@ async fn handle_head(
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "httpd/unix-directory")
|
||||
.header(header::CONTENT_LENGTH, 0)
|
||||
.header(header::ETAG, format!("\"{}\"", folder.id))
|
||||
.header(header::ETAG, format!("\"{}\"", folder.etag))
|
||||
.body(Body::empty())
|
||||
.unwrap());
|
||||
}
|
||||
@@ -756,7 +757,7 @@ async fn handle_head(
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, &*file.mime_type)
|
||||
.header(header::CONTENT_LENGTH, file.size)
|
||||
.header(header::ETAG, format!("\"{}\"", file.id))
|
||||
.header(header::ETAG, format!("\"{}\"", file.etag))
|
||||
.header(
|
||||
header::LAST_MODIFIED,
|
||||
chrono::DateTime::<Utc>::from_timestamp(file.created_at as i64, 0)
|
||||
@@ -777,7 +778,7 @@ async fn handle_head(
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "httpd/unix-directory")
|
||||
.header(header::CONTENT_LENGTH, 0)
|
||||
.header(header::ETAG, format!("\"{}\"", folder.id))
|
||||
.header(header::ETAG, format!("\"{}\"", folder.etag))
|
||||
.body(Body::empty())
|
||||
.unwrap());
|
||||
}
|
||||
@@ -793,7 +794,7 @@ async fn handle_head(
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, &*file.mime_type)
|
||||
.header(header::CONTENT_LENGTH, file.size)
|
||||
.header(header::ETAG, format!("\"{}\"", file.id))
|
||||
.header(header::ETAG, format!("\"{}\"", file.etag))
|
||||
.header(
|
||||
header::LAST_MODIFIED,
|
||||
chrono::DateTime::<Utc>::from_timestamp(file.created_at as i64, 0)
|
||||
|
||||
@@ -250,6 +250,9 @@ async fn handle_search(
|
||||
|
||||
/// Build a `FileDto` from a search file result.
|
||||
fn file_dto_from_search(fr: &crate::application::dtos::search_dto::SearchFileResultDto) -> FileDto {
|
||||
// `SearchFileResultDto` doesn't carry `blob_hash`; the SEARCH /
|
||||
// REPORT XML emitter doesn't read `content_hash` or `etag` off
|
||||
// these DTOs anyway, so leaving them empty here is correct.
|
||||
FileDto {
|
||||
id: fr.id.clone(),
|
||||
name: fr.name.clone(),
|
||||
@@ -267,6 +270,7 @@ fn file_dto_from_search(fr: &crate::application::dtos::search_dto::SearchFileRes
|
||||
size_formatted: format_file_size(fr.size),
|
||||
owner_id: None,
|
||||
sort_date: None,
|
||||
content_hash: String::new(),
|
||||
etag: String::new(),
|
||||
}
|
||||
}
|
||||
@@ -276,6 +280,7 @@ fn folder_dto_from_search(
|
||||
sr: &crate::application::dtos::search_dto::SearchFolderResultDto,
|
||||
) -> FolderDto {
|
||||
FolderDto {
|
||||
etag: sr.id.clone(),
|
||||
id: sr.id.clone(),
|
||||
name: sr.name.clone(),
|
||||
path: sr.path.clone(),
|
||||
|
||||
@@ -348,11 +348,18 @@ async fn handle_get(
|
||||
chrono::DateTime::<Utc>::from_timestamp(timestamp_to_i64(file.modified_at), 0)
|
||||
.unwrap_or_else(Utc::now);
|
||||
|
||||
// ETag comes from `FileDto::etag` (populated from `File::etag()`
|
||||
// in the `From<File>` impl) — single source of truth, so GET,
|
||||
// HEAD, PUT-response, MOVE, and PROPFIND all emit byte-identical
|
||||
// values for the same file. NC's sync engine compares cached
|
||||
// PROPFIND ETags against GET/HEAD responses; using `file.id` here
|
||||
// (a UUID) while PROPFIND emitted the blob hash made NC see
|
||||
// every file as "remotely changed" on first descent.
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, file.mime_type.as_ref())
|
||||
.header(header::CONTENT_LENGTH, file.size)
|
||||
.header(header::ETAG, format!("\"{}\"", file.id))
|
||||
.header(header::ETAG, format!("\"{}\"", file.etag))
|
||||
.header(header::LAST_MODIFIED, modified_at.to_rfc2822())
|
||||
.body(Body::from_stream(std::pin::Pin::from(stream)))
|
||||
.unwrap())
|
||||
@@ -400,11 +407,14 @@ async fn handle_head(
|
||||
chrono::DateTime::<Utc>::from_timestamp(timestamp_to_i64(file.modified_at), 0)
|
||||
.unwrap_or_else(Utc::now);
|
||||
|
||||
// ETag comes from `FileDto::etag` — see the same comment block on
|
||||
// the GET handler. HEAD and GET must agree byte-for-byte; pulling
|
||||
// both from the same DTO field guarantees that.
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, file.mime_type.as_ref())
|
||||
.header(header::CONTENT_LENGTH, file.size)
|
||||
.header(header::ETAG, format!("\"{}\"", file.id))
|
||||
.header(header::ETAG, format!("\"{}\"", file.etag))
|
||||
.header(header::LAST_MODIFIED, modified_at.to_rfc2822())
|
||||
.body(Body::empty())
|
||||
.unwrap())
|
||||
@@ -850,9 +860,13 @@ async fn handle_move(
|
||||
let dest_internal = nc_to_internal_path(&user.username, &dest_subpath)?;
|
||||
let mut builder = Response::builder().status(StatusCode::CREATED);
|
||||
if let Ok(moved) = file_service.get_file_by_path(&dest_internal).await {
|
||||
// Route through `FileDto::etag` so the MOVE response
|
||||
// matches what a subsequent PROPFIND on the destination
|
||||
// will return — `moved.id` (UUID) would differ from the
|
||||
// blob hash and trigger NC's "remote changed" detection.
|
||||
builder = builder
|
||||
.header(header::ETAG, format!("\"{}\"", moved.id))
|
||||
.header("oc-etag", format!("\"{}\"", moved.id));
|
||||
.header(header::ETAG, format!("\"{}\"", moved.etag))
|
||||
.header("oc-etag", format!("\"{}\"", moved.etag));
|
||||
}
|
||||
|
||||
return Ok(builder.body(Body::empty()).unwrap());
|
||||
@@ -1094,7 +1108,10 @@ pub fn write_folder_response<W: std::io::Write>(
|
||||
.unwrap_or_else(Utc::now);
|
||||
|
||||
write_text_element(xml, "d:getlastmodified", &modified_at.to_rfc2822())?;
|
||||
write_text_element(xml, "d:getetag", &format!("\"{}\"", folder.id))?;
|
||||
// Route through `FolderDto::etag` (= `Folder::etag()`, currently
|
||||
// the folder UUID — see the entity for the documented v1 formula
|
||||
// and the follow-up plan to make it descendant-aware).
|
||||
write_text_element(xml, "d:getetag", &format!("\"{}\"", folder.etag))?;
|
||||
write_text_element(xml, "d:getcontenttype", "httpd/unix-directory")?;
|
||||
write_text_element(xml, "d:getcontentlength", "0")?;
|
||||
write_text_element(xml, "d:creationdate", &created_at.to_rfc3339())?;
|
||||
|
||||
Reference in New Issue
Block a user