feat(group-quota): per-group shared-collection quotas (M3)
Global env-var ceilings enforced in the group write path (mirrors the per-value caps): PICLOUD_GROUP_KV_MAX_ROWS / _DOCS_MAX_ROWS (per-group row count, checked only on a NEW key/doc — updates exempt) and _FILES_MAX_TOTAL_BYTES (per-group total blob bytes). New group_quota env helpers; count_rows/total_bytes repo methods (trait-default 0 for non-Postgres); QuotaExceeded error variants on the three group error enums; a with_max_rows test builder. Pinned by a group_kv_service unit test (3rd key rejected, update of an existing key at the cap allowed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,9 @@ pub struct GroupFilesServiceImpl {
|
||||
resolver: Arc<dyn GroupCollectionResolver>,
|
||||
authz: Arc<dyn AuthzRepo>,
|
||||
max_file_size_bytes: usize,
|
||||
/// §11.6 per-group quota: max total stored bytes across the group's
|
||||
/// shared-files collections (`PICLOUD_GROUP_FILES_MAX_TOTAL_BYTES`).
|
||||
max_total_bytes: u64,
|
||||
/// §11.6: fires `shared = true` files triggers on a shared-collection write.
|
||||
events: Arc<dyn ServiceEventEmitter>,
|
||||
}
|
||||
@@ -47,6 +50,7 @@ impl GroupFilesServiceImpl {
|
||||
resolver,
|
||||
authz,
|
||||
max_file_size_bytes,
|
||||
max_total_bytes: crate::group_quota::group_files_max_total_bytes_from_env(),
|
||||
events: Arc::new(NoopEventEmitter),
|
||||
}
|
||||
}
|
||||
@@ -160,6 +164,15 @@ impl GroupFilesService for GroupFilesServiceImpl {
|
||||
// shape checks pass (same as app files, audit 2026-06-11 C-2).
|
||||
new.content_type = sanitize_stored_content_type(&new.content_type);
|
||||
self.check_write(cx, group_id).await?;
|
||||
// §11.6 quota: the new file's bytes must fit under the group's total.
|
||||
let used = self.repo.total_bytes(group_id).await?;
|
||||
let incoming = new.data.len() as u64;
|
||||
if used.saturating_add(incoming) > self.max_total_bytes {
|
||||
return Err(GroupFilesError::QuotaExceeded {
|
||||
limit: self.max_total_bytes,
|
||||
actual: used.saturating_add(incoming),
|
||||
});
|
||||
}
|
||||
let meta = self.repo.create(group_id, collection, new).await?;
|
||||
self.emit_shared(cx, group_id, "create", collection, &meta, None)
|
||||
.await;
|
||||
|
||||
Reference in New Issue
Block a user