Fix Nextcloud sync conflict by using content-hash ETags
The Nextcloud Android client compares ETags before and after upload to verify its write landed. OxiCloud was returning the stable file UUID as the ETag, which never changed on content updates, causing false SYNC_CONFLICT errors on every upload. Five fixes applied: 1. Thread blob_hash (SHA-256) through File entity, FileDto, all read/write queries, and all WebDAV/PROPFIND responses as the ETag — changes on every content update, no DB migration needed. 2. Honor X-OC-Mtime header: parse the client-supplied mtime and use it for updated_at via COALESCE(to_timestamp($n), NOW()). 3. Disable phantom checksum capability (preferredUploadType/supportedTypes) that the server never actually implemented, stopping retry loops. 4. Add nc:creation_time and nc:upload_time to PROPFIND responses. 5. Return oc-etag header in chunked upload MOVE (assemble) responses. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -243,7 +243,7 @@ impl Calendar {
|
||||
pub fn update_color(&mut self, color: Option<String>) -> Result<()> {
|
||||
// Validate color format if provided
|
||||
if let Some(color_str) = &color {
|
||||
Self::validate_color(&color_str)?;
|
||||
Self::validate_color(color_str)?;
|
||||
}
|
||||
|
||||
self.color = color;
|
||||
|
||||
@@ -21,6 +21,7 @@ pub struct FileParts {
|
||||
pub created_at: u64,
|
||||
pub modified_at: u64,
|
||||
pub owner_id: Option<Uuid>,
|
||||
pub etag: String,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,6 +65,9 @@ pub struct File {
|
||||
|
||||
/// Owner user ID (from storage.files.user_id)
|
||||
owner_id: Option<Uuid>,
|
||||
|
||||
/// Content-addressable ETag (= blob_hash). Changes on every content write.
|
||||
etag: String,
|
||||
}
|
||||
|
||||
// We no longer need this module, now we use a String directly
|
||||
@@ -81,6 +85,7 @@ impl Default for File {
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
owner_id: None,
|
||||
etag: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,6 +124,7 @@ impl File {
|
||||
created_at: now,
|
||||
modified_at: now,
|
||||
owner_id: None,
|
||||
etag: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -150,6 +156,7 @@ impl File {
|
||||
created_at,
|
||||
modified_at,
|
||||
owner_id: None,
|
||||
etag: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -164,6 +171,33 @@ impl File {
|
||||
created_at: u64,
|
||||
modified_at: u64,
|
||||
owner_id: Option<Uuid>,
|
||||
) -> FileResult<Self> {
|
||||
Self::with_timestamps_and_etag(
|
||||
id,
|
||||
name,
|
||||
storage_path,
|
||||
size,
|
||||
mime_type,
|
||||
folder_id,
|
||||
created_at,
|
||||
modified_at,
|
||||
owner_id,
|
||||
String::new(),
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn with_timestamps_and_etag(
|
||||
id: String,
|
||||
name: String,
|
||||
storage_path: StoragePath,
|
||||
size: u64,
|
||||
mime_type: String,
|
||||
folder_id: Option<String>,
|
||||
created_at: u64,
|
||||
modified_at: u64,
|
||||
owner_id: Option<Uuid>,
|
||||
etag: String,
|
||||
) -> FileResult<Self> {
|
||||
// Validate file name
|
||||
if name.is_empty() || name.contains('/') || name.contains('\\') {
|
||||
@@ -184,6 +218,7 @@ impl File {
|
||||
created_at,
|
||||
modified_at,
|
||||
owner_id,
|
||||
etag,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -203,9 +238,14 @@ impl File {
|
||||
created_at: self.created_at,
|
||||
modified_at: self.modified_at,
|
||||
owner_id: self.owner_id,
|
||||
etag: self.etag,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn etag(&self) -> &str {
|
||||
&self.etag
|
||||
}
|
||||
|
||||
// Getters
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
@@ -273,6 +313,7 @@ impl File {
|
||||
created_at,
|
||||
modified_at,
|
||||
owner_id: None,
|
||||
etag: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,6 +352,7 @@ impl File {
|
||||
created_at: self.created_at,
|
||||
modified_at: now,
|
||||
owner_id: self.owner_id,
|
||||
etag: self.etag.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -345,6 +387,7 @@ impl File {
|
||||
created_at: self.created_at,
|
||||
modified_at: now,
|
||||
owner_id: self.owner_id,
|
||||
etag: self.etag.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -366,6 +409,7 @@ impl File {
|
||||
created_at: self.created_at,
|
||||
modified_at: now,
|
||||
owner_id: self.owner_id,
|
||||
etag: self.etag.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user