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:
@@ -17,6 +17,7 @@ type MediaFileRow = (
|
||||
String, // mime_type
|
||||
i64, // created_at
|
||||
i64, // updated_at
|
||||
String, // blob_hash
|
||||
Option<Uuid>, // user_id
|
||||
i64, // sort_date
|
||||
);
|
||||
@@ -38,6 +39,7 @@ use crate::infrastructure::services::dedup_service::DedupService;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Type alias for file metadata rows from SQL queries.
|
||||
/// Fields: id, name, folder_id, folder_path, size, mime_type, created_at, updated_at, blob_hash, user_id
|
||||
type FileRow = (
|
||||
String,
|
||||
String,
|
||||
@@ -47,6 +49,7 @@ type FileRow = (
|
||||
String,
|
||||
i64,
|
||||
i64,
|
||||
String,
|
||||
Option<Uuid>,
|
||||
);
|
||||
|
||||
@@ -114,10 +117,11 @@ impl FileBlobReadRepository {
|
||||
mime_type: String,
|
||||
created_at: i64,
|
||||
modified_at: i64,
|
||||
etag: String,
|
||||
owner_id: Option<Uuid>,
|
||||
) -> Result<File, DomainError> {
|
||||
let storage_path = Self::make_file_path(folder_path.as_deref(), &name);
|
||||
File::with_timestamps(
|
||||
File::with_timestamps_and_etag(
|
||||
id,
|
||||
name,
|
||||
storage_path,
|
||||
@@ -127,6 +131,7 @@ impl FileBlobReadRepository {
|
||||
created_at as u64,
|
||||
modified_at as u64,
|
||||
owner_id,
|
||||
etag,
|
||||
)
|
||||
.map_err(|e| DomainError::internal_error("FileBlobRead", format!("entity: {e}")))
|
||||
}
|
||||
@@ -183,6 +188,7 @@ impl FileBlobReadRepository {
|
||||
fi.size, fi.mime_type,
|
||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||
fi.blob_hash,
|
||||
fi.user_id,
|
||||
EXTRACT(EPOCH FROM fi.media_sort_date)::bigint AS sort_date
|
||||
FROM storage.files fi
|
||||
@@ -206,9 +212,9 @@ impl FileBlobReadRepository {
|
||||
let mut files = Vec::with_capacity(rows.len());
|
||||
let mut sort_dates = Vec::with_capacity(rows.len());
|
||||
|
||||
for (id, name, fid, fpath, size, mime, ca, ma, uid, sd) in rows {
|
||||
for (id, name, fid, fpath, size, mime, ca, ma, etag, uid, sd) in rows {
|
||||
files.push(Self::row_to_file(
|
||||
id, name, fid, fpath, size, mime, ca, ma, uid,
|
||||
id, name, fid, fpath, size, mime, ca, ma, etag, uid,
|
||||
)?);
|
||||
sort_dates.push(sd);
|
||||
}
|
||||
@@ -257,7 +263,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
self.hash_cache.insert(id.to_string(), row.8.clone());
|
||||
|
||||
Self::row_to_file(
|
||||
row.0, row.1, row.2, row.3, row.4, row.5, row.6, row.7, row.9,
|
||||
row.0, row.1, row.2, row.3, row.4, row.5, row.6, row.7, row.8, row.9,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -302,7 +308,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
self.hash_cache.insert(id.to_string(), row.8.clone());
|
||||
|
||||
Self::row_to_file(
|
||||
row.0, row.1, row.2, row.3, row.4, row.5, row.6, row.7, row.9,
|
||||
row.0, row.1, row.2, row.3, row.4, row.5, row.6, row.7, row.8, row.9,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -315,6 +321,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
fi.size, fi.mime_type,
|
||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||
fi.blob_hash,
|
||||
fi.user_id
|
||||
FROM storage.files fi
|
||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||
@@ -332,6 +339,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
fi.size, fi.mime_type,
|
||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||
fi.blob_hash,
|
||||
fi.user_id
|
||||
FROM storage.files fi
|
||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||
@@ -345,8 +353,8 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
.map_err(|e| DomainError::internal_error("FileBlobRead", format!("list: {e}")))?;
|
||||
|
||||
rows.into_iter()
|
||||
.map(|(id, name, fid, fpath, size, mime, ca, ma, uid)| {
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, uid)
|
||||
.map(|(id, name, fid, fpath, size, mime, ca, ma, etag, uid)| {
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, etag, uid)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -365,6 +373,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
fi.size, fi.mime_type,
|
||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||
fi.blob_hash,
|
||||
fi.user_id
|
||||
FROM storage.files fi
|
||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||
@@ -384,6 +393,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
fi.size, fi.mime_type,
|
||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||
fi.blob_hash,
|
||||
fi.user_id
|
||||
FROM storage.files fi
|
||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||
@@ -399,8 +409,8 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
.map_err(|e| DomainError::internal_error("FileBlobRead", format!("list_for_owner: {e}")))?;
|
||||
|
||||
rows.into_iter()
|
||||
.map(|(id, name, fid, fpath, size, mime, ca, ma, uid)| {
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, uid)
|
||||
.map(|(id, name, fid, fpath, size, mime, ca, ma, etag, uid)| {
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, etag, uid)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -427,6 +437,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
fi.size, fi.mime_type,
|
||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||
fi.blob_hash,
|
||||
fi.user_id
|
||||
FROM storage.files fi
|
||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||
@@ -447,6 +458,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
fi.size, fi.mime_type,
|
||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||
fi.blob_hash,
|
||||
fi.user_id
|
||||
FROM storage.files fi
|
||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||
@@ -463,8 +475,8 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
.map_err(|e| DomainError::internal_error("FileBlobRead", format!("list_batch: {e}")))?;
|
||||
|
||||
rows.into_iter()
|
||||
.map(|(id, name, fid, fpath, size, mime, ca, ma, uid)| {
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, uid)
|
||||
.map(|(id, name, fid, fpath, size, mime, ca, ma, etag, uid)| {
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, etag, uid)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -485,6 +497,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
fi.size, fi.mime_type,
|
||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||
fi.blob_hash,
|
||||
fi.user_id
|
||||
FROM storage.files fi
|
||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||
@@ -507,6 +520,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
fi.size, fi.mime_type,
|
||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||
fi.blob_hash,
|
||||
fi.user_id
|
||||
FROM storage.files fi
|
||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||
@@ -527,8 +541,8 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
})?;
|
||||
|
||||
rows.into_iter()
|
||||
.map(|(id, name, fid, fpath, size, mime, ca, ma, uid)| {
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, uid)
|
||||
.map(|(id, name, fid, fpath, size, mime, ca, ma, etag, uid)| {
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, etag, uid)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -645,6 +659,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
String,
|
||||
i64,
|
||||
i64,
|
||||
String,
|
||||
Option<Uuid>,
|
||||
),
|
||||
>(
|
||||
@@ -653,6 +668,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
fi.size, fi.mime_type,
|
||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||
fi.blob_hash,
|
||||
fi.user_id
|
||||
FROM storage.files fi
|
||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||
@@ -675,6 +691,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
String,
|
||||
i64,
|
||||
i64,
|
||||
String,
|
||||
Option<Uuid>,
|
||||
),
|
||||
>(
|
||||
@@ -683,6 +700,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
fi.size, fi.mime_type,
|
||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||
fi.blob_hash,
|
||||
fi.user_id
|
||||
FROM storage.files fi
|
||||
JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||
@@ -698,7 +716,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
|
||||
match row {
|
||||
Some(r) => Ok(Some(Self::row_to_file(
|
||||
r.0, r.1, r.2, r.3, r.4, r.5, r.6, r.7, r.8,
|
||||
r.0, r.1, r.2, r.3, r.4, r.5, r.6, r.7, r.8, r.9,
|
||||
)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
@@ -718,13 +736,14 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
let stream = async_stream::try_stream! {
|
||||
let mut row_stream = sqlx::query_as::<_, (
|
||||
String, String, Option<String>, Option<String>,
|
||||
i64, String, i64, i64, Option<Uuid>,
|
||||
i64, String, i64, i64, String, Option<Uuid>,
|
||||
)>(
|
||||
r#"
|
||||
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
|
||||
fi.size, fi.mime_type,
|
||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||
fi.blob_hash,
|
||||
fi.user_id
|
||||
FROM storage.files fi
|
||||
JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||
@@ -739,9 +758,9 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
while let Some(row) = row_stream.try_next().await.map_err(|e| {
|
||||
DomainError::internal_error("FileBlobRead", format!("subtree stream: {e}"))
|
||||
})? {
|
||||
let (id, name, fid, fpath, size, mime, ca, ma, uid) = row;
|
||||
let (id, name, fid, fpath, size, mime, ca, ma, etag, uid) = row;
|
||||
let file = FileBlobReadRepository::row_to_file(
|
||||
id, name, fid, fpath, size, mime, ca, ma, uid,
|
||||
id, name, fid, fpath, size, mime, ca, ma, etag, uid,
|
||||
)?;
|
||||
yield file;
|
||||
}
|
||||
@@ -803,6 +822,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
fi.size, fi.mime_type, \
|
||||
EXTRACT(EPOCH FROM fi.created_at)::bigint, \
|
||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint, \
|
||||
fi.blob_hash, \
|
||||
fi.user_id, \
|
||||
COUNT(*) OVER() AS total_count \
|
||||
FROM storage.files fi \
|
||||
@@ -824,6 +844,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
String,
|
||||
i64,
|
||||
i64,
|
||||
String,
|
||||
Option<Uuid>,
|
||||
i64,
|
||||
),
|
||||
@@ -847,13 +868,15 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
.map_err(|e| DomainError::internal_error("FileBlobRead", format!("search: {e}")))?;
|
||||
|
||||
// total_count is the same in every row; 0 when result set is empty.
|
||||
let total_count = rows.first().map_or(0, |r| r.9) as usize;
|
||||
let total_count = rows.first().map_or(0, |r| r.10) as usize;
|
||||
|
||||
let files = rows
|
||||
.into_iter()
|
||||
.map(|(id, name, fid, fpath, size, mime, ca, ma, uid, _total)| {
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, uid)
|
||||
})
|
||||
.map(
|
||||
|(id, name, fid, fpath, size, mime, ca, ma, etag, uid, _total)| {
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, etag, uid)
|
||||
},
|
||||
)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| DomainError::internal_error("FileBlobRead", format!("mapping: {e}")))?;
|
||||
|
||||
@@ -964,6 +987,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
fi.size, fi.mime_type, \
|
||||
EXTRACT(EPOCH FROM fi.created_at)::bigint, \
|
||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint, \
|
||||
fi.blob_hash, \
|
||||
fi.user_id, \
|
||||
COUNT(*) OVER() AS total_count \
|
||||
FROM storage.files fi \
|
||||
@@ -985,6 +1009,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
String,
|
||||
i64,
|
||||
i64,
|
||||
String,
|
||||
Option<Uuid>,
|
||||
i64,
|
||||
),
|
||||
@@ -1029,13 +1054,15 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
DomainError::internal_error("FileBlobRead", format!("subtree search: {e}"))
|
||||
})?;
|
||||
|
||||
let total_count = rows.first().map_or(0, |r| r.9) as usize;
|
||||
let total_count = rows.first().map_or(0, |r| r.10) as usize;
|
||||
|
||||
let files = rows
|
||||
.into_iter()
|
||||
.map(|(id, name, fid, fpath, size, mime, ca, ma, uid, _total)| {
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, uid)
|
||||
})
|
||||
.map(
|
||||
|(id, name, fid, fpath, size, mime, ca, ma, etag, uid, _total)| {
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, etag, uid)
|
||||
},
|
||||
)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| {
|
||||
DomainError::internal_error("FileBlobRead", format!("subtree mapping: {e}"))
|
||||
@@ -1074,6 +1101,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
fi.size, fi.mime_type,
|
||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||
fi.blob_hash,
|
||||
fi.user_id
|
||||
FROM storage.files fi
|
||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||
@@ -1102,6 +1130,7 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
fi.size, fi.mime_type,
|
||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||
fi.blob_hash,
|
||||
fi.user_id
|
||||
FROM storage.files fi
|
||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||
@@ -1126,8 +1155,8 @@ impl FileReadPort for FileBlobReadRepository {
|
||||
.map_err(|e| DomainError::internal_error("FileBlobRead", format!("suggest: {e}")))?;
|
||||
|
||||
rows.into_iter()
|
||||
.map(|(id, name, fid, fpath, size, mime, ca, ma, uid)| {
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, uid)
|
||||
.map(|(id, name, fid, fpath, size, mime, ca, ma, etag, uid)| {
|
||||
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, etag, uid)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -100,9 +100,10 @@ impl FileBlobWriteRepository {
|
||||
created_at: i64,
|
||||
modified_at: i64,
|
||||
owner_id: Option<Uuid>,
|
||||
etag: String,
|
||||
) -> Result<File, DomainError> {
|
||||
let storage_path = Self::make_file_path(folder_path.as_deref(), &name);
|
||||
File::with_timestamps(
|
||||
File::with_timestamps_and_etag(
|
||||
id,
|
||||
name,
|
||||
storage_path,
|
||||
@@ -112,6 +113,7 @@ impl FileBlobWriteRepository {
|
||||
created_at as u64,
|
||||
modified_at as u64,
|
||||
owner_id,
|
||||
etag,
|
||||
)
|
||||
.map_err(|e| DomainError::internal_error("FileBlobWrite", format!("entity: {e}")))
|
||||
}
|
||||
@@ -132,12 +134,16 @@ impl FileBlobWriteRepository {
|
||||
/// Uses a CTE to capture the old hash before updating so the old blob
|
||||
/// reference can be decremented afterwards. Compensates on failure by
|
||||
/// removing the new blob reference.
|
||||
///
|
||||
/// `modified_at`: if `Some`, sets `updated_at` to that Unix timestamp;
|
||||
/// if `None`, uses `NOW()` (server time). Returns the new hash on success.
|
||||
async fn swap_blob_hash(
|
||||
&self,
|
||||
file_id: &str,
|
||||
new_hash: &str,
|
||||
new_size: i64,
|
||||
) -> Result<(), DomainError> {
|
||||
modified_at: Option<i64>,
|
||||
) -> Result<String, DomainError> {
|
||||
// Atomic CTE: capture old hash then update in one round-trip, no TOCTOU.
|
||||
let old_hash = match sqlx::query_scalar::<_, String>(
|
||||
r#"
|
||||
@@ -145,7 +151,8 @@ impl FileBlobWriteRepository {
|
||||
SELECT id, blob_hash FROM storage.files WHERE id = $3::uuid FOR UPDATE
|
||||
)
|
||||
UPDATE storage.files f
|
||||
SET blob_hash = $1, size = $2, updated_at = NOW()
|
||||
SET blob_hash = $1, size = $2,
|
||||
updated_at = COALESCE(to_timestamp($4), NOW())
|
||||
FROM old
|
||||
WHERE f.id = old.id
|
||||
RETURNING old.blob_hash
|
||||
@@ -154,6 +161,7 @@ impl FileBlobWriteRepository {
|
||||
.bind(new_hash)
|
||||
.bind(new_size)
|
||||
.bind(file_id)
|
||||
.bind(modified_at.map(|t| t as f64))
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
{
|
||||
@@ -192,7 +200,7 @@ impl FileBlobWriteRepository {
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(new_hash.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,6 +285,7 @@ impl FileWritePort for FileBlobWriteRepository {
|
||||
row.1,
|
||||
row.2,
|
||||
Some(user_id),
|
||||
blob_hash.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -314,6 +323,7 @@ impl FileWritePort for FileBlobWriteRepository {
|
||||
row.5,
|
||||
row.6,
|
||||
None,
|
||||
String::new(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -407,6 +417,7 @@ impl FileWritePort for FileBlobWriteRepository {
|
||||
row.5,
|
||||
row.6,
|
||||
None,
|
||||
row.7,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -446,6 +457,7 @@ impl FileWritePort for FileBlobWriteRepository {
|
||||
row.5,
|
||||
row.6,
|
||||
None,
|
||||
String::new(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -474,7 +486,8 @@ impl FileWritePort for FileBlobWriteRepository {
|
||||
size: u64,
|
||||
content_type: Option<String>,
|
||||
pre_computed_hash: Option<String>,
|
||||
) -> Result<(), DomainError> {
|
||||
modified_at: Option<i64>,
|
||||
) -> Result<String, DomainError> {
|
||||
// Streaming: pass pre-computed hash so dedup skips re-reading the file.
|
||||
let dedup_result = self
|
||||
.dedup
|
||||
@@ -482,7 +495,8 @@ impl FileWritePort for FileBlobWriteRepository {
|
||||
.await?;
|
||||
let new_hash = dedup_result.hash().to_string();
|
||||
|
||||
self.swap_blob_hash(file_id, &new_hash, size as i64).await
|
||||
self.swap_blob_hash(file_id, &new_hash, size as i64, modified_at)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn register_file_deferred(
|
||||
@@ -528,6 +542,7 @@ impl FileWritePort for FileBlobWriteRepository {
|
||||
row.1,
|
||||
row.2,
|
||||
Some(user_id),
|
||||
String::new(),
|
||||
)?;
|
||||
|
||||
// The target_path is not meaningful for blob storage (content goes to .blobs/)
|
||||
|
||||
@@ -175,6 +175,7 @@ impl PathResolverService {
|
||||
size_formatted: format_file_size(sz),
|
||||
owner_id: uid,
|
||||
sort_date: None,
|
||||
etag: String::new(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user