1use super::ContentToken;
4use crate::SnapshotId;
5use crate::{
6 AbsolutePath, AccessGrants, AccessRevisionNo, ActorId, AttributeKey, AttributeRevisionNo,
7 AttributeValue, BindingGeneration, ChangeSeq, CheckpointId, CommitId, ContentRef, DisplayName,
8 InodeId, ManifestNo, NamespaceId, RevisionNo, WriterEpoch, WriterId,
9};
10use crate::{NamespaceAccess, PrincipalId, PrincipalScope};
11use serde::{Deserialize, Serialize};
12use std::collections::BTreeMap;
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
17#[cfg_attr(feature = "openapi", schema(as = ErrorResponse))]
18pub struct ApiError {
19 pub code: String,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
23 #[cfg_attr(feature = "openapi", schema(nullable = false))]
24 pub feature: Option<String>,
25 pub message: String,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
29 #[cfg_attr(feature = "openapi", schema(nullable = false))]
30 pub param: Option<String>,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 #[cfg_attr(feature = "openapi", schema(nullable = false))]
34 pub request_id: Option<String>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 #[cfg_attr(feature = "openapi", schema(nullable = false))]
38 pub details: Option<Box<ErrorDetails>>,
39}
40
41#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
43#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
44pub struct ErrorDetails {
45 #[serde(default, skip_serializing_if = "Option::is_none")]
47 #[cfg_attr(feature = "openapi", schema(nullable = false))]
48 pub commit_id: Option<CommitId>,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
51 #[cfg_attr(feature = "openapi", schema(nullable = false))]
52 pub committed_seq: Option<ChangeSeq>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
55 #[cfg_attr(feature = "openapi", schema(nullable = false))]
56 pub committed_fingerprint: Option<String>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 #[cfg_attr(feature = "openapi", schema(nullable = false))]
60 pub operation_index: Option<u32>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
63 #[cfg_attr(feature = "openapi", schema(nullable = false))]
64 pub precondition_index: Option<u32>,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
67 #[cfg_attr(feature = "openapi", schema(nullable = false))]
68 pub fenced_writer_epoch: Option<WriterEpoch>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 #[cfg_attr(feature = "openapi", schema(nullable = false))]
72 pub active_writer_epoch: Option<WriterEpoch>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
75 #[cfg_attr(feature = "openapi", schema(nullable = false))]
76 pub active_writer: Option<WriterId>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
79 #[cfg_attr(feature = "openapi", schema(nullable = false))]
80 pub active_acquired_at_ms: Option<u64>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
83 #[cfg_attr(feature = "openapi", schema(nullable = false))]
84 pub max_writer_sessions: Option<usize>,
85 #[serde(
87 default,
88 skip_serializing_if = "Option::is_none",
89 with = "crate::public_inode_id::option"
90 )]
91 #[cfg_attr(feature = "openapi", schema(nullable = false))]
92 pub inode_id: Option<InodeId>,
93 #[serde(
95 default,
96 skip_serializing_if = "Option::is_none",
97 with = "crate::public_inode_id::option"
98 )]
99 #[cfg_attr(feature = "openapi", schema(nullable = false))]
100 pub expected_inode_id: Option<InodeId>,
101 #[serde(
103 default,
104 skip_serializing_if = "Option::is_none",
105 with = "crate::public_inode_id::option"
106 )]
107 #[cfg_attr(feature = "openapi", schema(nullable = false))]
108 pub actual_inode_id: Option<InodeId>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 #[cfg_attr(feature = "openapi", schema(nullable = false))]
112 pub expected_binding_generation: Option<BindingGeneration>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 #[cfg_attr(feature = "openapi", schema(nullable = false))]
116 pub actual_binding_generation: Option<BindingGeneration>,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
119 #[cfg_attr(feature = "openapi", schema(nullable = false))]
120 pub expected_revision_no: Option<RevisionNo>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
123 #[cfg_attr(feature = "openapi", schema(nullable = false))]
124 pub actual_revision_no: Option<RevisionNo>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 #[cfg_attr(feature = "openapi", schema(nullable = false))]
128 pub expected_attributes_revision_no: Option<AttributeRevisionNo>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
131 #[cfg_attr(feature = "openapi", schema(nullable = false))]
132 pub actual_attributes_revision_no: Option<AttributeRevisionNo>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
135 #[cfg_attr(feature = "openapi", schema(nullable = false))]
136 pub expected_access_revision_no: Option<AccessRevisionNo>,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
139 #[cfg_attr(feature = "openapi", schema(nullable = false))]
140 pub actual_access_revision_no: Option<AccessRevisionNo>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
143 #[cfg_attr(feature = "openapi", schema(nullable = false))]
144 pub after_seq: Option<ChangeSeq>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
147 #[cfg_attr(feature = "openapi", schema(nullable = false))]
148 pub retention_floor_seq: Option<ChangeSeq>,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
151 #[cfg_attr(feature = "openapi", schema(nullable = false))]
152 pub expected_deletion_seq: Option<ChangeSeq>,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
155 #[cfg_attr(feature = "openapi", schema(nullable = false))]
156 pub actual_deletion_seq: Option<ChangeSeq>,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
159 #[cfg_attr(feature = "openapi", schema(nullable = false))]
160 pub expected_head_seq: Option<ChangeSeq>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
163 #[cfg_attr(feature = "openapi", schema(nullable = false))]
164 pub actual_head_seq: Option<ChangeSeq>,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
169#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
170#[serde(deny_unknown_fields)]
171pub struct CreateNamespaceRequest {
172 #[serde(default = "NamespaceAccess::unrestricted")]
175 pub access: NamespaceAccess,
176 pub namespace_id: NamespaceId,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
182#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
183#[serde(deny_unknown_fields)]
184pub struct ForkNamespaceRequest {
185 pub new_namespace_id: NamespaceId,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
189 #[cfg_attr(feature = "openapi", schema(nullable = false))]
190 pub snapshot_id: Option<SnapshotId>,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
196pub struct Namespace {
197 pub access: NamespaceAccessMode,
199 pub namespace_id: NamespaceId,
201 pub created_at_ms: u64,
203 pub created_by: ActorId,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
207 #[cfg_attr(feature = "openapi", schema(nullable = false))]
208 pub fork_basis: Option<NamespaceForkBasis>,
209 pub head_seq: ChangeSeq,
211 pub retention_floor_seq: ChangeSeq,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
218#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
219pub enum NamespaceAccessMode {
220 Unrestricted {},
222 Acl {
224 principal_scope: PrincipalScope,
226 },
227}
228
229impl From<&NamespaceAccess> for NamespaceAccessMode {
230 fn from(access: &NamespaceAccess) -> Self {
231 match access {
232 NamespaceAccess::Unrestricted {} => Self::Unrestricted {},
233 NamespaceAccess::Acl {
234 principal_scope, ..
235 } => Self::Acl {
236 principal_scope: principal_scope.clone(),
237 },
238 }
239 }
240}
241
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
245pub struct NamespaceForkBasis {
246 pub source_namespace_id: NamespaceId,
248 pub source_head_seq: ChangeSeq,
250}
251
252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
254#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
255pub struct NamespaceDiagnostics {
256 pub namespace_id: NamespaceId,
258 pub created_at_ms: u64,
260 pub created_by: ActorId,
262 #[serde(default, skip_serializing_if = "Option::is_none")]
264 #[cfg_attr(feature = "openapi", schema(nullable = false))]
265 pub fork_basis: Option<NamespaceForkBasis>,
266 pub head_seq: ChangeSeq,
268 pub retention_floor_seq: ChangeSeq,
270 #[serde(default, skip_serializing_if = "Option::is_none")]
272 #[cfg_attr(feature = "openapi", schema(nullable = false))]
273 pub current_manifest_no: Option<ManifestNo>,
274 pub wal_tail_segments: u64,
276 pub live_snapshots: u64,
278 pub live_checkpoints: u64,
280}
281
282#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
284#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
285pub struct DeleteNamespaceResponse {
286 pub namespace_id: NamespaceId,
288 pub head_seq: ChangeSeq,
290}
291
292#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
294#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
295#[serde(rename_all = "snake_case")]
296pub enum DestinationBehavior {
297 #[default]
299 NoReplace,
300 Replace,
302}
303
304#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
306#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
307pub struct DestinationPrecondition {
308 #[serde(default)]
310 pub behavior: DestinationBehavior,
311 #[serde(
313 rename = "expected_destination_inode_id",
314 default,
315 skip_serializing_if = "Option::is_none",
316 with = "crate::public_inode_id::option"
317 )]
318 #[cfg_attr(feature = "openapi", schema(nullable = false))]
319 pub expected_inode_id: Option<InodeId>,
320 #[serde(
322 rename = "expected_destination_revision_no",
323 default,
324 skip_serializing_if = "Option::is_none"
325 )]
326 #[cfg_attr(feature = "openapi", schema(nullable = false))]
327 pub expected_revision_no: Option<RevisionNo>,
328}
329
330#[derive(Debug, Clone, Copy, PartialEq, Eq)]
332pub enum PreconditionFields {
333 Put,
335 Destination,
337}
338
339impl PreconditionFields {
340 fn names(self) -> (&'static str, &'static str) {
341 match self {
342 Self::Put => ("expected_revision_no", "expected_inode_id"),
343 Self::Destination => (
344 "expected_destination_revision_no",
345 "expected_destination_inode_id",
346 ),
347 }
348 }
349}
350
351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub struct ExpectedFileState {
354 pub inode_id: InodeId,
356 pub revision_no: Option<RevisionNo>,
358}
359
360#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
362#[non_exhaustive]
363pub enum DestinationPreconditionError {
364 #[error("destination preconditions require replace behavior")]
366 PreconditionsRequireReplace {
367 field: &'static str,
369 },
370 #[error(
372 "`{revision_field}` names the revision of one inode; pair it with `{inode_field}` so the precondition names which inode"
373 )]
374 RevisionRequiresInode {
375 revision_field: &'static str,
377 inode_field: &'static str,
379 },
380}
381
382impl DestinationPrecondition {
383 pub fn resolve(
385 &self,
386 fields: PreconditionFields,
387 ) -> Result<Option<ExpectedFileState>, DestinationPreconditionError> {
388 if self.behavior == DestinationBehavior::NoReplace
389 && !matches!(
390 (self.expected_inode_id, self.expected_revision_no),
391 (None, None)
392 )
393 {
394 let (revision_field, inode_field) = fields.names();
395 return Err(DestinationPreconditionError::PreconditionsRequireReplace {
396 field: if self.expected_inode_id.is_some() {
397 inode_field
398 } else {
399 revision_field
400 },
401 });
402 }
403 let Some(inode_id) = self.expected_inode_id else {
404 if self.expected_revision_no.is_some() {
405 let (revision_field, inode_field) = fields.names();
406 return Err(DestinationPreconditionError::RevisionRequiresInode {
407 revision_field,
408 inode_field,
409 });
410 }
411 return Ok(None);
412 };
413 Ok(Some(ExpectedFileState {
414 inode_id,
415 revision_no: self.expected_revision_no,
416 }))
417 }
418}
419
420pub fn validate_attributes_precondition(
422 expected_inode_id: Option<InodeId>,
423 expected_attributes_revision_no: Option<AttributeRevisionNo>,
424) -> Result<(), DestinationPreconditionError> {
425 validate_revision_precondition(
426 expected_inode_id,
427 expected_attributes_revision_no.is_some(),
428 "expected_attributes_revision_no",
429 )
430}
431
432pub fn validate_access_precondition(
434 expected_inode_id: Option<InodeId>,
435 expected_access_revision_no: Option<AccessRevisionNo>,
436) -> Result<(), DestinationPreconditionError> {
437 validate_revision_precondition(
438 expected_inode_id,
439 expected_access_revision_no.is_some(),
440 "expected_access_revision_no",
441 )
442}
443
444fn validate_revision_precondition(
445 expected_inode_id: Option<InodeId>,
446 has_revision: bool,
447 revision_field: &'static str,
448) -> Result<(), DestinationPreconditionError> {
449 if has_revision && expected_inode_id.is_none() {
450 return Err(DestinationPreconditionError::RevisionRequiresInode {
451 revision_field,
452 inode_field: "expected_inode_id",
453 });
454 }
455 Ok(())
456}
457
458#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
460#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
461#[serde(rename_all = "snake_case")]
462pub enum DeleteDirectoryBehavior {
463 #[default]
465 NonRecursive,
466 Recursive,
468}
469
470#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
474#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
475#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
476pub enum FilesystemOperation {
477 #[cfg_attr(
479 feature = "openapi",
480 schema(title = "FilesystemOperationCreateDirectory")
481 )]
482 CreateDirectory {
483 path: AbsolutePath,
485 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
488 parents: bool,
489 },
490 #[cfg_attr(
492 feature = "openapi",
493 schema(title = "FilesystemOperationCreateDirectoryByInode")
494 )]
495 CreateDirectoryByInode {
496 #[serde(with = "crate::public_inode_id")]
498 parent_inode_id: InodeId,
499 display_name: DisplayName,
501 },
502 #[cfg_attr(feature = "openapi", schema(title = "FilesystemOperationPutFile"))]
505 PutFile {
506 path: AbsolutePath,
508 #[serde(default, skip_serializing_if = "Option::is_none")]
510 #[cfg_attr(feature = "openapi", schema(nullable = false))]
511 content_ref: Option<ContentRef>,
512 #[serde(
514 default,
515 skip_serializing_if = "Option::is_none",
516 with = "crate::base64_bytes"
517 )]
518 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = Byte, nullable = false))]
519 inline_content: Option<Vec<u8>>,
520 #[serde(default)]
522 behavior: DestinationBehavior,
523 #[serde(
525 default,
526 skip_serializing_if = "Option::is_none",
527 with = "crate::public_inode_id::option"
528 )]
529 #[cfg_attr(feature = "openapi", schema(nullable = false))]
530 expected_inode_id: Option<InodeId>,
531 #[serde(default, skip_serializing_if = "Option::is_none")]
533 #[cfg_attr(feature = "openapi", schema(nullable = false))]
534 expected_revision_no: Option<RevisionNo>,
535 },
536 #[cfg_attr(
539 feature = "openapi",
540 schema(title = "FilesystemOperationCreateFileByInode")
541 )]
542 CreateFileByInode {
543 #[serde(with = "crate::public_inode_id")]
545 parent_inode_id: InodeId,
546 display_name: DisplayName,
548 #[serde(default, skip_serializing_if = "Option::is_none")]
550 #[cfg_attr(feature = "openapi", schema(nullable = false))]
551 content_ref: Option<ContentRef>,
552 #[serde(
554 default,
555 skip_serializing_if = "Option::is_none",
556 with = "crate::base64_bytes"
557 )]
558 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = Byte, nullable = false))]
559 inline_content: Option<Vec<u8>>,
560 },
561 #[cfg_attr(
564 feature = "openapi",
565 schema(title = "FilesystemOperationPutFileRevisionByInode")
566 )]
567 PutFileRevisionByInode {
568 #[serde(with = "crate::public_inode_id")]
570 inode_id: InodeId,
571 #[serde(default, skip_serializing_if = "Option::is_none")]
573 #[cfg_attr(feature = "openapi", schema(nullable = false))]
574 content_ref: Option<ContentRef>,
575 #[serde(
577 default,
578 skip_serializing_if = "Option::is_none",
579 with = "crate::base64_bytes"
580 )]
581 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = Byte, nullable = false))]
582 inline_content: Option<Vec<u8>>,
583 expected_revision_no: RevisionNo,
585 },
586 #[cfg_attr(feature = "openapi", schema(title = "FilesystemOperationDeletePath"))]
588 DeletePath {
589 path: AbsolutePath,
591 #[serde(default)]
593 behavior: DeleteDirectoryBehavior,
594 #[serde(
596 default,
597 skip_serializing_if = "Option::is_none",
598 with = "crate::public_inode_id::option"
599 )]
600 #[cfg_attr(feature = "openapi", schema(nullable = false))]
601 expected_inode_id: Option<InodeId>,
602 },
603 #[cfg_attr(
605 feature = "openapi",
606 schema(title = "FilesystemOperationDeleteByInode")
607 )]
608 DeleteByInode {
609 #[serde(with = "crate::public_inode_id")]
611 inode_id: InodeId,
612 expected_binding_generation: BindingGeneration,
614 #[serde(default)]
616 behavior: DeleteDirectoryBehavior,
617 },
618 #[cfg_attr(feature = "openapi", schema(title = "FilesystemOperationMovePath"))]
620 MovePath {
621 source_path: AbsolutePath,
623 destination_path: AbsolutePath,
625 #[serde(flatten)]
627 precondition: DestinationPrecondition,
628 },
629 #[cfg_attr(feature = "openapi", schema(title = "FilesystemOperationMoveByInode"))]
631 MoveByInode {
632 #[serde(with = "crate::public_inode_id")]
634 inode_id: InodeId,
635 expected_binding_generation: BindingGeneration,
637 #[serde(with = "crate::public_inode_id")]
639 destination_parent_inode_id: InodeId,
640 destination_display_name: DisplayName,
642 #[serde(flatten)]
644 precondition: DestinationPrecondition,
645 },
646 #[cfg_attr(feature = "openapi", schema(title = "FilesystemOperationCopyPath"))]
648 CopyPath {
649 source_path: AbsolutePath,
651 destination_path: AbsolutePath,
653 #[serde(flatten)]
655 precondition: DestinationPrecondition,
656 },
657 #[cfg_attr(feature = "openapi", schema(title = "FilesystemOperationUndelete"))]
659 Undelete {
660 #[serde(with = "crate::public_inode_id")]
662 inode_id: InodeId,
663 deletion_seq: ChangeSeq,
665 #[serde(default, skip_serializing_if = "Option::is_none")]
667 #[cfg_attr(feature = "openapi", schema(nullable = false))]
668 destination_path: Option<AbsolutePath>,
669 },
670 #[cfg_attr(
672 feature = "openapi",
673 schema(title = "FilesystemOperationRestoreRevision")
674 )]
675 RestoreRevision {
676 path: AbsolutePath,
678 source_revision_no: RevisionNo,
680 },
681 #[cfg_attr(
683 feature = "openapi",
684 schema(title = "FilesystemOperationUpdateAttributes")
685 )]
686 UpdateAttributes {
687 path: AbsolutePath,
689 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
692 set: BTreeMap<AttributeKey, AttributeValue>,
693 #[serde(default, skip_serializing_if = "Vec::is_empty")]
695 remove: Vec<AttributeKey>,
696 #[serde(
698 default,
699 skip_serializing_if = "Option::is_none",
700 with = "crate::public_inode_id::option"
701 )]
702 #[cfg_attr(feature = "openapi", schema(nullable = false))]
703 expected_inode_id: Option<InodeId>,
704 #[serde(default, skip_serializing_if = "Option::is_none")]
706 #[cfg_attr(feature = "openapi", schema(nullable = false))]
707 expected_attributes_revision_no: Option<AttributeRevisionNo>,
708 },
709 #[cfg_attr(feature = "openapi", schema(title = "FilesystemOperationUpdateAccess"))]
712 UpdateAccess {
713 path: AbsolutePath,
715 boundary: bool,
717 grants: AccessGrants,
719 #[serde(
721 default,
722 skip_serializing_if = "Option::is_none",
723 with = "crate::public_inode_id::option"
724 )]
725 #[cfg_attr(feature = "openapi", schema(nullable = false))]
726 expected_inode_id: Option<InodeId>,
727 #[serde(default, skip_serializing_if = "Option::is_none")]
729 #[cfg_attr(feature = "openapi", schema(nullable = false))]
730 expected_access_revision_no: Option<AccessRevisionNo>,
731 },
732}
733
734impl FilesystemOperation {
735 pub const fn content_ref(&self) -> Option<&ContentRef> {
737 match self {
738 Self::PutFile { content_ref, .. }
739 | Self::CreateFileByInode { content_ref, .. }
740 | Self::PutFileRevisionByInode { content_ref, .. } => content_ref.as_ref(),
741 Self::CreateDirectory { .. }
742 | Self::CreateDirectoryByInode { .. }
743 | Self::DeletePath { .. }
744 | Self::DeleteByInode { .. }
745 | Self::MovePath { .. }
746 | Self::MoveByInode { .. }
747 | Self::CopyPath { .. }
748 | Self::Undelete { .. }
749 | Self::RestoreRevision { .. }
750 | Self::UpdateAttributes { .. }
751 | Self::UpdateAccess { .. } => None,
752 }
753 }
754}
755
756#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
760#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
761#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
762pub enum CommitPrecondition {
763 #[cfg_attr(feature = "openapi", schema(title = "CommitPreconditionNamespaceHead"))]
765 NamespaceHead {
766 expected_head_seq: ChangeSeq,
768 },
769 #[cfg_attr(feature = "openapi", schema(title = "CommitPreconditionFileRevision"))]
771 FileRevision {
772 #[serde(with = "crate::public_inode_id")]
774 inode_id: InodeId,
775 expected_revision_no: RevisionNo,
777 },
778 #[cfg_attr(feature = "openapi", schema(title = "CommitPreconditionPathBinding"))]
780 PathBinding {
781 path: AbsolutePath,
783 #[serde(with = "crate::public_inode_id")]
785 expected_inode_id: InodeId,
786 #[serde(default, skip_serializing_if = "Option::is_none")]
788 #[cfg_attr(feature = "openapi", schema(nullable = false))]
789 expected_binding_generation: Option<BindingGeneration>,
790 },
791 #[cfg_attr(
793 feature = "openapi",
794 schema(title = "CommitPreconditionAttributesRevision")
795 )]
796 AttributesRevision {
797 #[serde(with = "crate::public_inode_id")]
799 inode_id: InodeId,
800 expected_attributes_revision_no: AttributeRevisionNo,
802 },
803 #[cfg_attr(
805 feature = "openapi",
806 schema(title = "CommitPreconditionAccessRevision")
807 )]
808 AccessRevision {
809 #[serde(with = "crate::public_inode_id")]
811 inode_id: InodeId,
812 expected_access_revision_no: AccessRevisionNo,
814 },
815 #[cfg_attr(feature = "openapi", schema(title = "CommitPreconditionPathAbsence"))]
817 PathAbsence {
818 path: AbsolutePath,
820 },
821}
822
823#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
827#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
828#[serde(deny_unknown_fields)]
829pub struct CommitRequest {
830 pub commit_id: CommitId,
832 #[serde(default, skip_serializing_if = "Option::is_none")]
834 #[cfg_attr(feature = "openapi", schema(nullable = false))]
835 pub message: Option<String>,
836 #[serde(default, skip_serializing_if = "Vec::is_empty")]
838 pub content_tokens: Vec<ContentToken>,
839 #[serde(default, skip_serializing_if = "Vec::is_empty")]
841 pub preconditions: Vec<CommitPrecondition>,
842 pub operations: Vec<FilesystemOperation>,
844}
845
846impl CommitRequest {
847 pub fn preconditions(mut self, preconditions: Vec<CommitPrecondition>) -> Self {
849 self.preconditions = preconditions;
850 self
851 }
852
853 pub fn single(
855 commit_id: CommitId,
856 message: Option<String>,
857 operation: FilesystemOperation,
858 ) -> Self {
859 Self {
860 commit_id,
861 message,
862 content_tokens: Vec::new(),
863 preconditions: Vec::new(),
864 operations: vec![operation],
865 }
866 }
867}
868
869#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
871#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
872pub struct FileRevision {
873 #[serde(with = "crate::public_inode_id")]
875 pub inode_id: InodeId,
876 pub revision_no: RevisionNo,
878 pub committed_seq: ChangeSeq,
880 pub commit_id: CommitId,
882 pub committed_at_ms: u64,
884 pub committed_by: crate::ActorId,
886 pub content_ref: ContentRef,
888}
889
890#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
892#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
893pub struct ListFileRevisionsResponse {
894 pub namespace_id: NamespaceId,
896 #[serde(with = "crate::public_inode_id")]
898 pub inode_id: InodeId,
899 pub head_seq: ChangeSeq,
901 pub revisions: Vec<FileRevision>,
903 #[serde(default, skip_serializing_if = "Option::is_none")]
905 #[cfg_attr(feature = "openapi", schema(nullable = false))]
906 pub next_cursor: Option<String>,
907}
908
909#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
911#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
912#[serde(deny_unknown_fields)]
913pub struct CreateCheckpointRequest {
914 pub name: String,
916 #[serde(default, skip_serializing_if = "Option::is_none")]
918 #[cfg_attr(feature = "openapi", schema(nullable = false))]
919 pub ttl_ms: Option<u64>,
920}
921
922#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
924#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
925#[serde(deny_unknown_fields)]
926pub struct CreateSnapshotRequest {
927 pub name: String,
929 pub ttl_ms: u64,
931}
932
933#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
935#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
936#[serde(deny_unknown_fields)]
937pub struct ExtendSnapshotRequest {
938 pub ttl_ms: u64,
940}
941
942#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
944#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
945pub struct DeleteCheckpointResponse {
946 pub namespace_id: NamespaceId,
948 pub checkpoint_id: CheckpointId,
950}
951
952#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
954#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
955#[serde(tag = "kind", rename_all = "snake_case")]
956pub enum CheckpointOwnerSummary {
957 #[cfg_attr(feature = "openapi", schema(title = "CheckpointOwnerUser"))]
959 User {
960 name: String,
962 },
963 #[cfg_attr(feature = "openapi", schema(title = "CheckpointOwnerFork"))]
965 Fork {
966 target_namespace_id: NamespaceId,
968 },
969 #[cfg_attr(feature = "openapi", schema(title = "CheckpointOwnerSnapshot"))]
971 Snapshot {
972 name: String,
974 },
975}
976
977#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
979#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
980pub struct Checkpoint {
981 pub namespace_id: NamespaceId,
983 pub checkpoint_id: CheckpointId,
985 pub owner: CheckpointOwnerSummary,
987 pub created_at_ms: u64,
989 #[serde(default, skip_serializing_if = "Option::is_none")]
991 #[cfg_attr(feature = "openapi", schema(nullable = false))]
992 pub expires_at_ms: Option<u64>,
993 pub captured_seq: ChangeSeq,
995 pub manifest_no: ManifestNo,
997}
998
999#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1001#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1002#[cfg_attr(feature = "openapi", schema(as = Snapshot))]
1003pub struct SnapshotSummary {
1004 pub snapshot_id: SnapshotId,
1006 pub namespace_id: NamespaceId,
1008 pub name: String,
1010 pub captured_seq: ChangeSeq,
1012 pub created_at_ms: u64,
1014 pub expires_at_ms: u64,
1016}
1017
1018impl SnapshotSummary {
1019 pub fn from_checkpoint(checkpoint: Checkpoint) -> Option<Self> {
1024 let CheckpointOwnerSummary::Snapshot { name } = checkpoint.owner else {
1025 return None;
1026 };
1027 Some(Self {
1028 snapshot_id: checkpoint.checkpoint_id.into(),
1029 namespace_id: checkpoint.namespace_id,
1030 name,
1031 captured_seq: checkpoint.captured_seq,
1032 created_at_ms: checkpoint.created_at_ms,
1033 expires_at_ms: checkpoint.expires_at_ms?,
1034 })
1035 }
1036}
1037
1038#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1040#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1041pub struct ListCheckpointsResponse {
1042 pub namespace_id: NamespaceId,
1044 pub checkpoints: Vec<Checkpoint>,
1046 #[serde(default, skip_serializing_if = "Option::is_none")]
1048 #[cfg_attr(feature = "openapi", schema(nullable = false))]
1049 pub next_cursor: Option<String>,
1050}
1051
1052#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1054#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1055pub struct ListSnapshotsResponse {
1056 pub namespace_id: NamespaceId,
1058 pub snapshots: Vec<SnapshotSummary>,
1060 #[serde(default, skip_serializing_if = "Option::is_none")]
1062 #[cfg_attr(feature = "openapi", schema(nullable = false))]
1063 pub next_cursor: Option<String>,
1064}
1065
1066#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1068#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1069pub struct DeleteSnapshotResponse {
1070 pub namespace_id: NamespaceId,
1072 pub snapshot_id: SnapshotId,
1074}
1075
1076#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1078#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1079#[serde(rename_all = "snake_case")]
1080pub enum FlushWalOutcome {
1081 AlreadyCurrent,
1083 Published,
1085 ManifestAdvanced,
1087}
1088
1089#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1091#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1092pub struct FlushWalResponse {
1093 pub namespace_id: NamespaceId,
1095 pub target_head_seq: ChangeSeq,
1097 pub manifest_no: ManifestNo,
1099 pub manifest_head_seq: ChangeSeq,
1101 pub outcome: FlushWalOutcome,
1103}
1104
1105#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1109#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1110#[serde(deny_unknown_fields)]
1111pub struct GcRequest {
1112 #[serde(default, skip_serializing_if = "Option::is_none")]
1115 #[cfg_attr(feature = "openapi", schema(nullable = false))]
1116 pub grace_window_ms: Option<u64>,
1117}
1118
1119#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1121#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1122pub struct RetainedCandidates {
1123 pub referenced: u64,
1125 pub within_grace_window: u64,
1127 pub no_provider_timestamp: u64,
1129 pub unrecognized_key: u64,
1131 pub checkpoint_not_deletable: u64,
1133 pub upload_session_window: u64,
1135 pub upload_session_undecided: u64,
1137}
1138
1139#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1141#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1142pub struct DeletedObjectCounts {
1143 pub wal_segments: u64,
1145 pub metadata_segments: u64,
1147 pub manifests: u64,
1149 pub upload_sessions: u64,
1151 pub content_objects: u64,
1153 pub retired_content_objects: u64,
1155}
1156
1157impl DeletedObjectCounts {
1158 pub fn add(&mut self, other: &Self) {
1160 let Self {
1161 wal_segments,
1162 metadata_segments,
1163 manifests,
1164 upload_sessions,
1165 content_objects,
1166 retired_content_objects,
1167 } = other;
1168 self.wal_segments += wal_segments;
1169 self.metadata_segments += metadata_segments;
1170 self.manifests += manifests;
1171 self.upload_sessions += upload_sessions;
1172 self.content_objects += content_objects;
1173 self.retired_content_objects += retired_content_objects;
1174 }
1175}
1176
1177#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1179#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1180pub struct DeletedCheckpointsByOwner {
1181 pub fork: u64,
1183 pub expired: u64,
1185 pub snapshot: u64,
1187}
1188
1189impl DeletedCheckpointsByOwner {
1190 pub fn add(&mut self, other: &Self) {
1192 let Self {
1193 fork,
1194 expired,
1195 snapshot,
1196 } = other;
1197 self.fork += fork;
1198 self.expired += expired;
1199 self.snapshot += snapshot;
1200 }
1201}
1202
1203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1205#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1206pub struct GcResponse {
1207 pub namespace_id: NamespaceId,
1209 pub deleted: DeletedObjectCounts,
1211 pub deleted_checkpoints_by_owner: DeletedCheckpointsByOwner,
1213 pub retained: RetainedCandidates,
1215 #[serde(default, skip_serializing_if = "Option::is_none")]
1217 #[cfg_attr(feature = "openapi", schema(nullable = false))]
1218 pub next_reclamation_at_ms: Option<u64>,
1219 #[serde(default, skip_serializing_if = "Option::is_none")]
1221 #[cfg_attr(feature = "openapi", schema(nullable = false))]
1222 pub reclaim_after_ms: Option<u64>,
1223}
1224
1225impl GcResponse {
1226 pub fn empty(namespace_id: NamespaceId) -> Self {
1228 Self {
1229 namespace_id,
1230 deleted: DeletedObjectCounts::default(),
1231 deleted_checkpoints_by_owner: DeletedCheckpointsByOwner::default(),
1232 retained: RetainedCandidates::default(),
1233 next_reclamation_at_ms: None,
1234 reclaim_after_ms: None,
1235 }
1236 }
1237
1238 pub fn retain(&mut self, reason: RetainedReason) {
1240 *reason.counter(&mut self.retained) += 1;
1241 }
1242}
1243
1244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1246pub enum RetainedReason {
1247 Referenced,
1249 WithinGraceWindow,
1251 NoProviderTimestamp,
1253 UnrecognizedKey,
1255 CheckpointNotDeletable,
1257 UploadSessionWindow,
1259 UploadSessionUndecided,
1261}
1262
1263impl RetainedReason {
1264 fn counter(self, retained: &mut RetainedCandidates) -> &mut u64 {
1265 match self {
1266 Self::Referenced => &mut retained.referenced,
1267 Self::WithinGraceWindow => &mut retained.within_grace_window,
1268 Self::NoProviderTimestamp => &mut retained.no_provider_timestamp,
1269 Self::UnrecognizedKey => &mut retained.unrecognized_key,
1270 Self::CheckpointNotDeletable => &mut retained.checkpoint_not_deletable,
1271 Self::UploadSessionWindow => &mut retained.upload_session_window,
1272 Self::UploadSessionUndecided => &mut retained.upload_session_undecided,
1273 }
1274 }
1275}
1276
1277impl RetainedCandidates {
1278 pub fn total(&self) -> u64 {
1280 self.by_reason().into_iter().map(|(_, count)| count).sum()
1281 }
1282
1283 pub(crate) fn by_reason(&self) -> [(&'static str, u64); 7] {
1285 let Self {
1286 referenced,
1287 within_grace_window,
1288 no_provider_timestamp,
1289 unrecognized_key,
1290 checkpoint_not_deletable,
1291 upload_session_window,
1292 upload_session_undecided,
1293 } = *self;
1294 [
1295 ("referenced", referenced),
1296 ("within_grace_window", within_grace_window),
1297 ("no_provider_timestamp", no_provider_timestamp),
1298 ("unrecognized_key", unrecognized_key),
1299 ("checkpoint_not_deletable", checkpoint_not_deletable),
1300 ("upload_session_window", upload_session_window),
1301 ("upload_session_undecided", upload_session_undecided),
1302 ]
1303 }
1304
1305 pub fn add(&mut self, other: &Self) {
1307 let Self {
1308 referenced,
1309 within_grace_window,
1310 no_provider_timestamp,
1311 unrecognized_key,
1312 checkpoint_not_deletable,
1313 upload_session_window,
1314 upload_session_undecided,
1315 } = other;
1316 self.referenced += referenced;
1317 self.within_grace_window += within_grace_window;
1318 self.no_provider_timestamp += no_provider_timestamp;
1319 self.unrecognized_key += unrecognized_key;
1320 self.checkpoint_not_deletable += checkpoint_not_deletable;
1321 self.upload_session_window += upload_session_window;
1322 self.upload_session_undecided += upload_session_undecided;
1323 }
1324
1325 pub fn top_reason(&self) -> Option<(&'static str, u64)> {
1329 self.by_reason()
1330 .into_iter()
1331 .filter(|(_, count)| *count > 0)
1332 .rev()
1335 .max_by_key(|(_, count)| *count)
1336 }
1337}
1338
1339#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1341#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1342#[serde(deny_unknown_fields)]
1343pub struct AdvanceRetentionRequest {}
1344
1345#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1347#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1348pub struct AdvanceRetentionResponse {
1349 pub namespace_id: NamespaceId,
1351 pub retention_floor_seq: ChangeSeq,
1353}
1354
1355#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1357#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1358#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1359pub enum RunMaintenanceRequest {
1360 Metadata(MetadataMaintenanceRequest),
1362 MetadataCompaction(MetadataCompactionRequest),
1364 Gc(GcRequest),
1366 Retention(AdvanceRetentionRequest),
1368 RecoverAdministrator(RecoverAdministratorRequest),
1370}
1371
1372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1375#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1376#[serde(deny_unknown_fields)]
1377pub struct RecoverAdministratorRequest {
1378 pub principal_id: PrincipalId,
1380}
1381
1382#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1384#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1385pub struct RecoverAdministratorResponse {
1386 pub namespace_id: NamespaceId,
1388 pub commit_id: CommitId,
1390 pub committed_seq: ChangeSeq,
1392 pub access_revision_no: AccessRevisionNo,
1394}
1395
1396#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1398#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1399#[serde(deny_unknown_fields)]
1400pub struct MetadataMaintenanceRequest {
1401 #[serde(default, skip_serializing_if = "Option::is_none")]
1403 #[cfg_attr(feature = "openapi", schema(nullable = false))]
1404 pub max_wal_tail_segments: Option<u64>,
1405}
1406
1407#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1409#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1410#[serde(deny_unknown_fields)]
1411pub struct MetadataCompactionRequest {}
1412
1413#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1415#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1416#[serde(tag = "outcome", rename_all = "snake_case")]
1417pub enum WalFlushStepOutcome {
1418 NotNeeded,
1420 Flushed {
1422 manifest_head_seq: ChangeSeq,
1424 },
1425 AlreadyPublished {
1427 attempted_seq: ChangeSeq,
1429 current_manifest_no: ManifestNo,
1431 },
1432 RetriesExhausted {
1434 observed_head_seq: ChangeSeq,
1436 },
1437}
1438
1439#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1441#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1442#[serde(tag = "outcome", rename_all = "snake_case")]
1443pub enum ReorganizeStepOutcome {
1444 #[cfg_attr(feature = "openapi", schema(title = "ReorganizeStepOutcomeNotNeeded"))]
1446 NotNeeded {},
1447 #[cfg_attr(
1449 feature = "openapi",
1450 schema(title = "ReorganizeStepOutcomeUnitPublished")
1451 )]
1452 UnitPublished {},
1453 #[cfg_attr(
1455 feature = "openapi",
1456 schema(title = "ReorganizeStepOutcomeCompactionRequired")
1457 )]
1458 CompactionRequired {},
1459 #[cfg_attr(
1461 feature = "openapi",
1462 schema(title = "ReorganizeStepOutcomeManifestAdvanced")
1463 )]
1464 ManifestAdvanced {},
1465 #[cfg_attr(feature = "openapi", schema(title = "ReorganizeStepOutcomeFenced"))]
1467 Fenced {},
1468}
1469
1470#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1472#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1473#[serde(tag = "kind", rename_all = "snake_case")]
1474pub enum RunMaintenanceResponse {
1475 Metadata(MetadataMaintenanceResponse),
1477 MetadataCompaction(MetadataCompactionResponse),
1479 Gc(GcResponse),
1481 Retention(AdvanceRetentionResponse),
1483 RecoverAdministrator(RecoverAdministratorResponse),
1485}
1486
1487#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1489#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1490pub struct MetadataMaintenanceResponse {
1491 pub namespace_id: NamespaceId,
1493 pub wal_flush: WalFlushStepOutcome,
1495 pub reorganize: ReorganizeStepOutcome,
1497}
1498
1499#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1501#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1502pub struct MetadataCompactionResponse {
1503 pub namespace_id: NamespaceId,
1505 pub compaction: MetadataCompactionOutcome,
1507}
1508
1509#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1511#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1512#[serde(tag = "outcome", rename_all = "snake_case")]
1513pub enum MetadataCompactionOutcome {
1514 NotNeeded,
1516 BoundedMergePublished,
1518 Published {
1520 manifest_no: ManifestNo,
1522 rows_read: u64,
1524 rows_written: u64,
1526 input_bytes: u64,
1528 output_bytes: u64,
1530 output_segments: u64,
1532 },
1533 Cancelled,
1535 Abandoned,
1537 Fenced,
1539}
1540
1541#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1543#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1544#[serde(deny_unknown_fields)]
1545pub struct StoreProbeRequest {}
1546
1547#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1549#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1550pub struct StoreProbeResponse {
1551 pub run_id: String,
1553 pub checks: Vec<StoreProbeCheckResult>,
1555}
1556
1557#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1559#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1560pub struct StoreProbeCheckResult {
1561 pub name: String,
1563 pub outcome: StoreProbeCheckOutcome,
1565 #[serde(default, skip_serializing_if = "Option::is_none")]
1567 #[cfg_attr(feature = "openapi", schema(nullable = false))]
1568 pub message: Option<String>,
1569}
1570
1571#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1573#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1574#[serde(rename_all = "snake_case")]
1575pub enum StoreProbeCheckOutcome {
1576 Passed,
1578 Unsupported,
1580 Failed,
1582}
1583
1584#[cfg(test)]
1585mod tests {
1586 use super::*;
1587 use crate::ContentId;
1588
1589 #[test]
1590 fn file_revision_provenance_fields_are_pinned_on_the_wire() {
1591 let content_ref = ContentRef::blob_v1(
1592 crate::NamespaceId::parse("demo").expect("namespace id"),
1593 crate::ContentId::generate(),
1594 b"hello",
1595 );
1596 let revision = FileRevision {
1597 inode_id: InodeId(2),
1598 revision_no: RevisionNo(3),
1599 committed_seq: ChangeSeq(7),
1600 commit_id: CommitId::parse("c_revision_owner").expect("commit id"),
1601 committed_at_ms: 1_752_624_000_000,
1602 committed_by: crate::ActorId::loonfs(),
1603 content_ref: content_ref.clone(),
1604 };
1605
1606 assert_eq!(
1607 serde_json::to_value(revision).expect("serialize file revision"),
1608 serde_json::json!({
1609 "inode_id": "ino_2",
1610 "revision_no": 3,
1611 "committed_seq": 7,
1612 "commit_id": "c_revision_owner",
1613 "committed_at_ms": 1_752_624_000_000_u64,
1614 "committed_by": "loonfs",
1615 "content_ref": content_ref,
1616 })
1617 );
1618 }
1619 fn path(value: &str) -> AbsolutePath {
1620 AbsolutePath::parse(value).expect("valid test path")
1621 }
1622
1623 fn attribute_key(value: &str) -> AttributeKey {
1624 AttributeKey::parse(value).expect("valid test attribute key")
1625 }
1626
1627 fn sample_content_ref() -> ContentRef {
1628 ContentRef::blob_v1(
1629 crate::NamespaceId::parse("demo").expect("namespace id"),
1630 ContentId::parse("con_0123456789abcdef0123456789abcdef").expect("valid content id"),
1631 b"hello",
1632 )
1633 }
1634
1635 #[test]
1636 fn namespace_wire_shape_has_only_core_state() {
1637 let namespace = Namespace {
1638 access: NamespaceAccessMode::Unrestricted {},
1639 namespace_id: NamespaceId::parse("demo").expect("namespace id"),
1640 created_at_ms: 1_000,
1641 created_by: crate::ActorId::parse("test").expect("actor"),
1642 fork_basis: None,
1643 head_seq: ChangeSeq(11),
1644 retention_floor_seq: ChangeSeq(4),
1645 };
1646 assert_eq!(
1647 serde_json::to_value(namespace).expect("serialize namespace"),
1648 serde_json::json!({
1649 "namespace_id": "demo",
1650 "access": {"kind": "unrestricted"},
1651 "created_at_ms": 1000,
1652 "created_by": "test",
1653 "head_seq": 11,
1654 "retention_floor_seq": 4
1655 })
1656 );
1657 }
1658
1659 #[test]
1660 fn namespace_diagnostics_wire_shape_keeps_storage_fields() {
1661 let diagnostics = NamespaceDiagnostics {
1662 namespace_id: NamespaceId::parse("demo").expect("namespace id"),
1663 created_at_ms: 1_000,
1664 created_by: crate::ActorId::parse("test").expect("actor"),
1665 fork_basis: None,
1666 head_seq: ChangeSeq(11),
1667 retention_floor_seq: ChangeSeq(4),
1668 current_manifest_no: Some(ManifestNo(8)),
1669 wal_tail_segments: 3,
1670 live_snapshots: 2,
1671 live_checkpoints: 5,
1672 };
1673 assert_eq!(
1674 serde_json::to_value(diagnostics).expect("serialize namespace diagnostics"),
1675 serde_json::json!({
1676 "namespace_id": "demo",
1677 "created_at_ms": 1000,
1678 "created_by": "test",
1679 "head_seq": 11,
1680 "retention_floor_seq": 4,
1681 "current_manifest_no": 8,
1682 "wal_tail_segments": 3,
1683 "live_snapshots": 2,
1684 "live_checkpoints": 5
1685 })
1686 );
1687 }
1688
1689 #[test]
1690 fn behavior_enums_use_snake_case_wire_values() {
1691 assert_eq!(
1692 DestinationBehavior::default(),
1693 DestinationBehavior::NoReplace
1694 );
1695 assert_eq!(
1696 DeleteDirectoryBehavior::default(),
1697 DeleteDirectoryBehavior::NonRecursive
1698 );
1699 assert_eq!(
1700 serde_json::to_value(DestinationBehavior::NoReplace)
1701 .expect("destination behavior json"),
1702 serde_json::json!("no_replace")
1703 );
1704 assert_eq!(
1705 serde_json::to_value(DestinationBehavior::Replace).expect("destination behavior json"),
1706 serde_json::json!("replace")
1707 );
1708 assert_eq!(
1709 serde_json::to_value(DeleteDirectoryBehavior::NonRecursive)
1710 .expect("delete behavior json"),
1711 serde_json::json!("non_recursive")
1712 );
1713 assert_eq!(
1714 serde_json::to_value(DeleteDirectoryBehavior::Recursive).expect("delete behavior json"),
1715 serde_json::json!("recursive")
1716 );
1717 }
1718
1719 #[test]
1720 fn filesystem_delete_and_move_operations_use_behavior_field() {
1721 let create_directory = FilesystemOperation::CreateDirectory {
1722 path: path("/docs"),
1723 parents: false,
1724 };
1725 assert_eq!(
1726 serde_json::to_value(&create_directory).expect("create directory op json"),
1727 serde_json::json!({
1728 "kind": "create_directory",
1729 "path": "/docs"
1730 })
1731 );
1732
1733 let create_directory_with_parents = FilesystemOperation::CreateDirectory {
1734 path: path("/docs/notes"),
1735 parents: true,
1736 };
1737 assert_eq!(
1738 serde_json::to_value(&create_directory_with_parents)
1739 .expect("create directory with parents op json"),
1740 serde_json::json!({
1741 "kind": "create_directory",
1742 "path": "/docs/notes",
1743 "parents": true
1744 })
1745 );
1746
1747 let delete = FilesystemOperation::DeletePath {
1748 path: path("/docs"),
1749 behavior: DeleteDirectoryBehavior::Recursive,
1750 expected_inode_id: None,
1751 };
1752 assert_eq!(
1753 serde_json::to_value(&delete).expect("delete op json"),
1754 serde_json::json!({
1755 "kind": "delete_path",
1756 "path": "/docs",
1757 "behavior": "recursive"
1758 })
1759 );
1760
1761 let move_path = FilesystemOperation::MovePath {
1762 source_path: path("/docs/a.txt"),
1763 destination_path: path("/docs/b.txt"),
1764 precondition: crate::DestinationPrecondition {
1765 behavior: DestinationBehavior::Replace,
1766 expected_inode_id: Some(InodeId(7)),
1767 expected_revision_no: Some(RevisionNo(3)),
1768 },
1769 };
1770 assert_eq!(
1771 serde_json::to_value(&move_path).expect("move op json"),
1772 serde_json::json!({
1773 "kind": "move_path",
1774 "source_path": "/docs/a.txt",
1775 "destination_path": "/docs/b.txt",
1776 "behavior": "replace",
1777 "expected_destination_inode_id": "ino_7",
1778 "expected_destination_revision_no": 3
1779 })
1780 );
1781
1782 let copy_path = FilesystemOperation::CopyPath {
1783 source_path: path("/docs/a.txt"),
1784 destination_path: path("/docs/b.txt"),
1785 precondition: crate::DestinationPrecondition {
1786 behavior: DestinationBehavior::Replace,
1787 expected_inode_id: Some(InodeId(7)),
1788 expected_revision_no: Some(RevisionNo(3)),
1789 },
1790 };
1791 assert_eq!(
1792 serde_json::to_value(©_path).expect("copy op json"),
1793 serde_json::json!({
1794 "kind": "copy_path",
1795 "source_path": "/docs/a.txt",
1796 "destination_path": "/docs/b.txt",
1797 "behavior": "replace",
1798 "expected_destination_inode_id": "ino_7",
1799 "expected_destination_revision_no": 3
1800 })
1801 );
1802
1803 let update_attributes = FilesystemOperation::UpdateAttributes {
1804 path: path("/docs/a.txt"),
1805 set: BTreeMap::from([(
1806 attribute_key("owner"),
1807 AttributeValue::parse("ada").expect("valid attribute value"),
1808 )]),
1809 remove: vec![attribute_key("draft")],
1810 expected_inode_id: Some(InodeId(7)),
1811 expected_attributes_revision_no: Some(AttributeRevisionNo(3)),
1812 };
1813 assert_eq!(
1814 serde_json::to_value(&update_attributes).expect("update attributes op json"),
1815 serde_json::json!({
1816 "kind": "update_attributes",
1817 "path": "/docs/a.txt",
1818 "set": {"owner": "ada"},
1819 "remove": ["draft"],
1820 "expected_inode_id": "ino_7",
1821 "expected_attributes_revision_no": 3
1822 })
1823 );
1824 }
1825
1826 #[test]
1827 fn update_attributes_omits_empty_collections_and_absent_preconditions() {
1828 let set_only = FilesystemOperation::UpdateAttributes {
1829 path: path("/docs/a.txt"),
1830 set: BTreeMap::from([(
1831 attribute_key("owner"),
1832 AttributeValue::parse("ada,grace").expect("valid attribute value"),
1833 )]),
1834 remove: Vec::new(),
1835 expected_inode_id: None,
1836 expected_attributes_revision_no: None,
1837 };
1838 assert_eq!(
1839 serde_json::to_value(&set_only).expect("set-only op json"),
1840 serde_json::json!({
1841 "kind": "update_attributes",
1842 "path": "/docs/a.txt",
1843 "set": {"owner": "ada,grace"}
1844 })
1845 );
1846
1847 let decoded: FilesystemOperation = serde_json::from_value(serde_json::json!({
1848 "kind": "update_attributes",
1849 "path": "/docs/a.txt",
1850 "remove": ["draft"]
1851 }))
1852 .expect("remove-only op defaults the set map and both preconditions");
1853 assert_eq!(
1854 decoded,
1855 FilesystemOperation::UpdateAttributes {
1856 path: path("/docs/a.txt"),
1857 set: BTreeMap::new(),
1858 remove: vec![attribute_key("draft")],
1859 expected_inode_id: None,
1860 expected_attributes_revision_no: None,
1861 }
1862 );
1863 }
1864
1865 #[test]
1866 fn update_attributes_validates_keys_and_values_during_deserialization() {
1867 for encoded in [
1870 serde_json::json!({
1871 "kind": "update_attributes",
1872 "path": "/docs/a.txt",
1873 "set": {"": "ada"}
1874 }),
1875 serde_json::json!({
1876 "kind": "update_attributes",
1877 "path": "/docs/a.txt",
1878 "set": {"owner": {"kind": "string", "value": "ada"}}
1879 }),
1880 serde_json::json!({
1881 "kind": "update_attributes",
1882 "path": "/docs/a.txt",
1883 "remove": ["a\u{0}b"]
1884 }),
1885 ] {
1886 assert!(serde_json::from_value::<FilesystemOperation>(encoded).is_err());
1887 }
1888 }
1889
1890 #[test]
1891 fn filesystem_operations_default_omitted_behavior_fields() {
1892 let put: FilesystemOperation = serde_json::from_value(serde_json::json!({
1893 "kind": "put_file",
1894 "path": "/docs/a.txt",
1895 "content_ref": {
1896 "kind": "blob_v1",
1897 "owner_namespace_id": "demo",
1898 "content_id": "con_0123456789abcdef0123456789abcdef",
1899 "size_bytes": 1,
1900 "checksum": {
1901 "algorithm": "sha256",
1902 "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1903 }
1904 }
1905 }))
1906 .expect("put op defaults behavior");
1907 assert!(matches!(
1908 put,
1909 FilesystemOperation::PutFile {
1910 behavior: DestinationBehavior::NoReplace,
1911 expected_inode_id: None,
1912 expected_revision_no: None,
1913 ..
1914 }
1915 ));
1916
1917 let delete: FilesystemOperation = serde_json::from_value(serde_json::json!({
1918 "kind": "delete_path",
1919 "path": "/docs"
1920 }))
1921 .expect("delete op defaults behavior");
1922 assert_eq!(
1923 delete,
1924 FilesystemOperation::DeletePath {
1925 path: path("/docs"),
1926 behavior: DeleteDirectoryBehavior::NonRecursive,
1927 expected_inode_id: None,
1928 }
1929 );
1930
1931 let move_path: FilesystemOperation = serde_json::from_value(serde_json::json!({
1932 "kind": "move_path",
1933 "source_path": "/docs/a.txt",
1934 "destination_path": "/docs/b.txt"
1935 }))
1936 .expect("move op defaults behavior");
1937 assert_eq!(
1938 move_path,
1939 FilesystemOperation::MovePath {
1940 source_path: path("/docs/a.txt"),
1941 destination_path: path("/docs/b.txt"),
1942 precondition: crate::DestinationPrecondition {
1943 behavior: DestinationBehavior::NoReplace,
1944 expected_inode_id: None,
1945 expected_revision_no: None,
1946 },
1947 }
1948 );
1949
1950 let copy_path: FilesystemOperation = serde_json::from_value(serde_json::json!({
1951 "kind": "copy_path",
1952 "source_path": "/docs/a.txt",
1953 "destination_path": "/docs/b.txt"
1954 }))
1955 .expect("copy op defaults behavior");
1956 assert_eq!(
1957 copy_path,
1958 FilesystemOperation::CopyPath {
1959 source_path: path("/docs/a.txt"),
1960 destination_path: path("/docs/b.txt"),
1961 precondition: crate::DestinationPrecondition {
1962 behavior: DestinationBehavior::NoReplace,
1963 expected_inode_id: None,
1964 expected_revision_no: None,
1965 },
1966 }
1967 );
1968
1969 let move_by_inode: FilesystemOperation = serde_json::from_value(serde_json::json!({
1970 "kind": "move_by_inode",
1971 "inode_id": "ino_7",
1972 "expected_binding_generation": "aaaa",
1973 "destination_parent_inode_id": "ino_1",
1974 "destination_display_name": "b.txt"
1975 }))
1976 .expect("inode move defaults behavior");
1977 assert_eq!(
1978 move_by_inode,
1979 FilesystemOperation::MoveByInode {
1980 inode_id: InodeId(7),
1981 expected_binding_generation: BindingGeneration::parse("aaaa")
1982 .expect("binding generation"),
1983 destination_parent_inode_id: InodeId(1),
1984 destination_display_name: DisplayName::parse("b.txt").expect("display name"),
1985 precondition: DestinationPrecondition::default(),
1986 }
1987 );
1988 }
1989
1990 #[test]
1991 fn filesystem_operation_paths_keep_the_plain_string_wire_shape() {
1992 let content_ref = ContentRef::blob_v1(
1993 crate::NamespaceId::parse("demo").expect("namespace id"),
1994 ContentId::generate(),
1995 b"hello",
1996 );
1997 let cases = [
1998 (
1999 FilesystemOperation::PutFile {
2000 path: path("/docs/a.txt"),
2001 content_ref: Some(content_ref.clone()),
2002 inline_content: None,
2003 behavior: DestinationBehavior::NoReplace,
2004 expected_inode_id: None,
2005 expected_revision_no: None,
2006 },
2007 serde_json::json!({
2008 "kind": "put_file",
2009 "path": "/docs/a.txt",
2010 "content_ref": content_ref,
2011 "behavior": "no_replace"
2012 }),
2013 ),
2014 (
2015 FilesystemOperation::Undelete {
2016 inode_id: InodeId(7),
2017 deletion_seq: ChangeSeq(8),
2018 destination_path: Some(path("/docs/restored")),
2019 },
2020 serde_json::json!({
2021 "kind": "undelete",
2022 "inode_id": "ino_7",
2023 "deletion_seq": 8,
2024 "destination_path": "/docs/restored"
2025 }),
2026 ),
2027 (
2028 FilesystemOperation::RestoreRevision {
2029 path: path("/docs/a.txt"),
2030 source_revision_no: RevisionNo(2),
2031 },
2032 serde_json::json!({
2033 "kind": "restore_revision",
2034 "path": "/docs/a.txt",
2035 "source_revision_no": 2
2036 }),
2037 ),
2038 (
2039 FilesystemOperation::UpdateAttributes {
2040 path: path("/docs/a.txt"),
2041 set: BTreeMap::new(),
2042 remove: vec![attribute_key("draft")],
2043 expected_inode_id: None,
2044 expected_attributes_revision_no: None,
2045 },
2046 serde_json::json!({
2047 "kind": "update_attributes",
2048 "path": "/docs/a.txt",
2049 "remove": ["draft"]
2050 }),
2051 ),
2052 ];
2053
2054 for (operation, string_shaped_json) in cases {
2055 assert_eq!(
2056 serde_json::to_value(operation).expect("serialize filesystem operation"),
2057 string_shaped_json
2058 );
2059 }
2060 }
2061
2062 #[test]
2063 fn filesystem_operation_paths_validate_during_deserialization() {
2064 for encoded in [
2065 serde_json::json!({"kind": "create_directory", "path": "relative", "parents": false}),
2066 serde_json::json!({
2067 "kind": "put_file",
2068 "path": "relative",
2069 "content_ref": ContentRef::blob_v1(crate::NamespaceId::parse("demo").expect("namespace id"), ContentId::generate(), b"hello")
2070 }),
2071 serde_json::json!({"kind": "delete_path", "path": "relative"}),
2072 serde_json::json!({
2073 "kind": "move_path",
2074 "source_path": "relative",
2075 "destination_path": "/target"
2076 }),
2077 serde_json::json!({
2078 "kind": "copy_path",
2079 "source_path": "/source",
2080 "destination_path": "relative"
2081 }),
2082 serde_json::json!({
2083 "kind": "undelete",
2084 "inode_id": "ino_7",
2085 "deletion_seq": 8,
2086 "destination_path": "relative"
2087 }),
2088 serde_json::json!({
2089 "kind": "restore_revision",
2090 "path": "relative",
2091 "source_revision_no": 2
2092 }),
2093 serde_json::json!({
2094 "kind": "update_attributes",
2095 "path": "relative",
2096 "remove": ["draft"]
2097 }),
2098 ] {
2099 assert!(serde_json::from_value::<FilesystemOperation>(encoded).is_err());
2100 }
2101 }
2102
2103 #[test]
2104 fn inode_request_fields_accept_only_the_public_format() {
2105 let operations = [
2106 serde_json::json!({
2107 "kind": "delete_path",
2108 "path": "/docs/a.txt",
2109 "expected_inode_id": "ino_27"
2110 }),
2111 serde_json::json!({
2112 "kind": "undelete",
2113 "inode_id": "ino_27",
2114 "deletion_seq": 8,
2115 "destination_path": "/docs/restored"
2116 }),
2117 serde_json::json!({
2118 "kind": "update_attributes",
2119 "path": "/docs/a.txt",
2120 "expected_inode_id": "ino_27"
2121 }),
2122 ];
2123
2124 for operation in operations {
2125 serde_json::from_value::<FilesystemOperation>(operation.clone())
2126 .expect("valid public inode ID");
2127
2128 let inode_key = if operation["kind"] == "undelete" {
2129 "inode_id"
2130 } else {
2131 "expected_inode_id"
2132 };
2133 for invalid in [serde_json::json!(27), serde_json::json!("27")] {
2134 let mut invalid_operation = operation.clone();
2135 invalid_operation[inode_key] = invalid;
2136 assert!(
2137 serde_json::from_value::<FilesystemOperation>(invalid_operation).is_err(),
2138 "{inode_key} accepted an invalid inode ID"
2139 );
2140 }
2141 }
2142 }
2143
2144 #[test]
2145 fn path_preconditions_reject_ambiguous_shapes() {
2146 let missing = serde_json::from_value::<CommitPrecondition>(
2147 serde_json::json!({"kind": "path_binding", "path": "/docs/input"}),
2148 )
2149 .expect_err("binding requires an inode");
2150 assert!(
2151 missing.to_string().contains("expected_inode_id"),
2152 "{missing}"
2153 );
2154 serde_json::from_value::<CommitPrecondition>(serde_json::json!({
2155 "kind": "path_binding", "path": "/docs/input", "expected_inode_id": null
2156 }))
2157 .expect_err("a null inode is not an absence check");
2158 let error = serde_json::from_value::<CommitPrecondition>(serde_json::json!({
2159 "kind": "path_absence", "path": "/docs/input", "expected_inode_id": "ino_42"
2160 }))
2161 .expect_err("absence accepts only a path");
2162 assert!(
2163 error
2164 .to_string()
2165 .contains("unknown field `expected_inode_id`"),
2166 "{error}"
2167 );
2168 }
2169
2170 #[test]
2171 fn a_misspelled_precondition_does_not_decode() {
2172 let put = |precondition: &str| {
2173 let mut operation = serde_json::json!({
2174 "kind": "put_file",
2175 "path": "/docs/a.txt",
2176 "content_ref": sample_content_ref(),
2177 "behavior": "replace",
2178 "expected_inode_id": "ino_7"
2179 });
2180 operation[precondition] = serde_json::json!(3);
2181 serde_json::json!({
2182 "commit_id": "with_preconditions-put",
2183 "operations": [operation]
2184 })
2185 };
2186
2187 let spelled: CommitRequest = serde_json::from_value(put("expected_revision_no"))
2188 .expect("the precondition spelled correctly decodes");
2189 assert!(matches!(
2190 spelled.operations.as_slice(),
2191 [FilesystemOperation::PutFile {
2192 expected_revision_no: Some(RevisionNo(3)),
2193 ..
2194 }]
2195 ));
2196
2197 for misspelling in ["expected_revsion_no", "expectedRevisionNo"] {
2198 assert!(
2199 serde_json::from_value::<CommitRequest>(put(misspelling)).is_err(),
2200 "`{misspelling}` decoded instead of failing the request"
2201 );
2202 }
2203 }
2204
2205 #[test]
2206 fn expected_revision_no_must_fit_the_public_integer_range() {
2207 let body = |expected_revision_no: u64| {
2208 serde_json::json!({
2209 "commit_id": "bounded-revision-precondition",
2210 "operations": [{
2211 "kind": "put_file",
2212 "path": "/docs/a.txt",
2213 "content_ref": sample_content_ref(),
2214 "behavior": "replace",
2215 "expected_inode_id": "ino_7",
2216 "expected_revision_no": expected_revision_no
2217 }]
2218 })
2219 };
2220
2221 let request: CommitRequest = serde_json::from_value(body(crate::MAX_PUBLIC_INTEGER))
2222 .expect("deserialize the maximum revision number");
2223 assert!(matches!(
2224 request.operations.as_slice(),
2225 [FilesystemOperation::PutFile {
2226 expected_revision_no: Some(RevisionNo(value)),
2227 ..
2228 }] if *value == crate::MAX_PUBLIC_INTEGER
2229 ));
2230
2231 let error = serde_json::from_value::<CommitRequest>(body(crate::MAX_PUBLIC_INTEGER + 1))
2232 .expect_err("reject a revision number above the public limit");
2233 assert!(
2234 error
2235 .to_string()
2236 .contains("must be an integer from 0 through 9007199254740991"),
2237 "unexpected range error: {error}"
2238 );
2239 }
2240
2241 #[test]
2242 fn a_commit_request_rejects_unknown_fields_at_every_level() {
2243 let valid = || {
2244 serde_json::json!({
2245 "commit_id": "strict-commit",
2246 "content_tokens": [{
2247 "content_ref": sample_content_ref(),
2248 "token": "opaque-proof"
2249 }],
2250 "operations": [{
2251 "kind": "update_attributes",
2252 "path": "/docs/a.txt",
2253 "set": {"owner": "ada"},
2254 "expected_inode_id": "ino_7"
2255 }]
2256 })
2257 };
2258 serde_json::from_value::<CommitRequest>(valid())
2259 .expect("the same body without a typo decodes");
2260
2261 let mut at_root = valid();
2262 at_root["mesage"] = serde_json::json!("a note");
2263
2264 let mut in_operation = valid();
2265 in_operation["operations"][0]["expectedAttributesRevisionNo"] = serde_json::json!(3);
2266
2267 let mut in_content_token = valid();
2268 in_content_token["content_tokens"][0]["expires_at_ms"] = serde_json::json!(1);
2269
2270 let mut in_content_ref = valid();
2271 in_content_ref["content_tokens"][0]["content_ref"]["sizeBytes"] = serde_json::json!(5);
2272
2273 for (level, body) in [
2274 ("the request root", at_root),
2275 ("an operation variant", in_operation),
2276 ("a nested content token", in_content_token),
2277 ("a content ref below that", in_content_ref),
2278 ] {
2279 assert!(
2280 serde_json::from_value::<CommitRequest>(body).is_err(),
2281 "an unknown field in {level} decoded instead of failing the request"
2282 );
2283 }
2284
2285 for (field, operation) in [
2286 (
2287 "path",
2288 serde_json::json!({
2289 "kind": "undelete",
2290 "inode_id": "ino_7",
2291 "deletion_seq": 8,
2292 "path": "/docs/restored"
2293 }),
2294 ),
2295 (
2296 "from_path",
2297 serde_json::json!({
2298 "kind": "move_path",
2299 "source_path": "/docs/a.txt",
2300 "destination_path": "/docs/b.txt",
2301 "from_path": "/docs/a.txt"
2302 }),
2303 ),
2304 ] {
2305 let mut body = valid();
2306 body["operations"] = serde_json::json!([operation]);
2307 let error = serde_json::from_value::<CommitRequest>(body)
2308 .expect_err("obsolete operation field must be rejected");
2309 assert!(
2310 error
2311 .to_string()
2312 .contains(&format!("unknown field `{field}`")),
2313 "{error}"
2314 );
2315 }
2316 }
2317
2318 #[test]
2319 fn checkpoint_responses_use_one_checkpoint_wire_object() {
2320 let namespace_id = NamespaceId::parse("demo").expect("namespace id");
2321 let checkpoint = Checkpoint {
2322 namespace_id: namespace_id.clone(),
2323 checkpoint_id: CheckpointId::parse("pin_00000000000000000001-0000000000000001")
2324 .expect("checkpoint id"),
2325 owner: CheckpointOwnerSummary::User {
2326 name: "release".to_owned(),
2327 },
2328 created_at_ms: 1_752_623_000_000,
2329 expires_at_ms: Some(1_752_626_600_000),
2330 captured_seq: ChangeSeq(12),
2331 manifest_no: ManifestNo(9),
2332 };
2333 let checkpoint_json = serde_json::json!({
2334 "namespace_id": "demo",
2335 "checkpoint_id": "pin_00000000000000000001-0000000000000001",
2336 "owner": {"kind": "user", "name": "release"},
2337 "created_at_ms": 1_752_623_000_000_u64,
2338 "expires_at_ms": 1_752_626_600_000_u64,
2339 "captured_seq": 12,
2340 "manifest_no": 9,
2341 });
2342 assert_eq!(
2343 serde_json::to_value(checkpoint.clone()).expect("serialize checkpoint"),
2344 checkpoint_json,
2345 );
2346 assert_eq!(
2347 serde_json::to_value(ListCheckpointsResponse {
2348 namespace_id: namespace_id.clone(),
2349 checkpoints: vec![checkpoint.clone()],
2350 next_cursor: None,
2351 })
2352 .expect("serialize list checkpoints response"),
2353 serde_json::json!({
2354 "namespace_id": "demo",
2355 "checkpoints": [checkpoint_json],
2356 }),
2357 );
2358 assert_eq!(
2359 serde_json::to_value(DeleteCheckpointResponse {
2360 namespace_id,
2361 checkpoint_id: checkpoint.checkpoint_id,
2362 })
2363 .expect("serialize delete checkpoint response"),
2364 serde_json::json!({
2365 "namespace_id": "demo",
2366 "checkpoint_id": "pin_00000000000000000001-0000000000000001",
2367 }),
2368 );
2369 }
2370
2371 #[test]
2372 fn optional_response_fields_are_omitted_and_default_when_absent() {
2373 let checkpoint_json = serde_json::to_value(Checkpoint {
2374 namespace_id: NamespaceId::parse("demo").expect("namespace id"),
2375 checkpoint_id: CheckpointId::parse("pin_00000000000000000001-0000000000000001")
2376 .expect("checkpoint id"),
2377 owner: CheckpointOwnerSummary::User {
2378 name: "release".to_owned(),
2379 },
2380 created_at_ms: 1_752_623_000_000,
2381 expires_at_ms: None,
2382 captured_seq: ChangeSeq(3),
2383 manifest_no: ManifestNo(3),
2384 })
2385 .expect("serialize checkpoint");
2386 assert!(checkpoint_json.get("expires_at_ms").is_none());
2387 let checkpoint: Checkpoint = serde_json::from_value(checkpoint_json)
2388 .expect("decode checkpoint without optional fields");
2389 assert_eq!(checkpoint.expires_at_ms, None);
2390
2391 let gc = GcResponse::empty(NamespaceId::parse("demo").expect("namespace id"));
2392 let gc_json = serde_json::to_value(gc).expect("serialize gc response");
2393 assert!(gc_json.get("next_reclamation_at_ms").is_none());
2394 assert!(gc_json.get("reclaim_after_ms").is_none());
2395 let gc: GcResponse =
2396 serde_json::from_value(gc_json).expect("decode gc response without optional fields");
2397 assert_eq!(gc.next_reclamation_at_ms, None);
2398 assert_eq!(gc.reclaim_after_ms, None);
2399 let retired = GcResponse {
2400 reclaim_after_ms: Some(2_000_000),
2401 ..gc
2402 };
2403 let json = serde_json::to_value(&retired).expect("encode retirement");
2404 assert_eq!(json["reclaim_after_ms"], 2_000_000);
2405 assert_eq!(
2406 serde_json::from_value::<GcResponse>(json).expect("decode retirement"),
2407 retired
2408 );
2409 }
2410
2411 #[test]
2412 fn maintenance_outcomes_use_the_outcome_tag() {
2413 assert_eq!(
2414 serde_json::to_value(WalFlushStepOutcome::Flushed {
2415 manifest_head_seq: ChangeSeq(9),
2416 })
2417 .expect("serialize WAL flush outcome"),
2418 serde_json::json!({"outcome": "flushed", "manifest_head_seq": 9})
2419 );
2420 assert_eq!(
2421 serde_json::to_value(ReorganizeStepOutcome::UnitPublished {})
2422 .expect("serialize reorganize outcome"),
2423 serde_json::json!({"outcome": "unit_published"})
2424 );
2425 assert_eq!(
2426 serde_json::to_value(RunMaintenanceResponse::MetadataCompaction(
2427 MetadataCompactionResponse {
2428 namespace_id: NamespaceId::parse("demo").expect("namespace id"),
2429 compaction: MetadataCompactionOutcome::Published {
2430 manifest_no: ManifestNo(7),
2431 rows_read: 11,
2432 rows_written: 9,
2433 input_bytes: 120,
2434 output_bytes: 80,
2435 output_segments: 2,
2436 },
2437 },
2438 ))
2439 .expect("serialize metadata compaction response"),
2440 serde_json::json!({
2441 "kind": "metadata_compaction",
2442 "namespace_id": "demo",
2443 "compaction": {
2444 "outcome": "published",
2445 "manifest_no": 7,
2446 "rows_read": 11,
2447 "rows_written": 9,
2448 "input_bytes": 120,
2449 "output_bytes": 80,
2450 "output_segments": 2
2451 }
2452 })
2453 );
2454 }
2455
2456 #[test]
2457 fn run_maintenance_requests_are_strict_and_round_trip() {
2458 let cases = [
2459 (
2460 serde_json::json!({"kind": "metadata"}),
2461 Some(RunMaintenanceRequest::Metadata(
2462 MetadataMaintenanceRequest::default(),
2463 )),
2464 ),
2465 (
2466 serde_json::json!({"kind": "metadata", "max_wal_tail_segments": 4}),
2467 Some(RunMaintenanceRequest::Metadata(
2468 MetadataMaintenanceRequest {
2469 max_wal_tail_segments: Some(4),
2470 },
2471 )),
2472 ),
2473 (
2474 serde_json::json!({"kind": "metadata_compaction"}),
2475 Some(RunMaintenanceRequest::MetadataCompaction(
2476 MetadataCompactionRequest {},
2477 )),
2478 ),
2479 (
2480 serde_json::json!({"kind": "gc"}),
2481 Some(RunMaintenanceRequest::Gc(GcRequest::default())),
2482 ),
2483 (
2484 serde_json::json!({
2485 "kind": "gc",
2486 "grace_window_ms": 600_000
2487 }),
2488 Some(RunMaintenanceRequest::Gc(GcRequest {
2489 grace_window_ms: Some(600_000),
2490 })),
2491 ),
2492 (
2493 serde_json::json!({"kind": "retention"}),
2494 Some(RunMaintenanceRequest::Retention(AdvanceRetentionRequest {})),
2495 ),
2496 (serde_json::json!({}), None),
2497 (serde_json::json!({"kind": "nope"}), None),
2498 (serde_json::json!({"kind": "gc", "bogus": 1}), None),
2499 (serde_json::json!({"kind": "gc", "max_objects": 1}), None),
2500 (serde_json::json!({"kind": "gc", "max_steps": 1}), None),
2501 (serde_json::json!({"kind": "retention", "bogus": 1}), None),
2502 (
2503 serde_json::json!({"kind": "metadata_compaction", "bogus": 1}),
2504 None,
2505 ),
2506 ];
2507
2508 for (body, expected) in cases {
2509 let decoded = serde_json::from_value::<RunMaintenanceRequest>(body.clone());
2510 match expected {
2511 Some(expected) => {
2512 let decoded = decoded.expect("valid maintenance request should decode");
2513 assert_eq!(decoded, expected);
2514 assert_eq!(
2515 serde_json::to_value(decoded)
2516 .expect("maintenance request should serialize"),
2517 body
2518 );
2519 }
2520 None => assert!(
2521 decoded.is_err(),
2522 "invalid maintenance request decoded: {body}"
2523 ),
2524 }
2525 }
2526
2527 serde_json::from_value::<CreateCheckpointRequest>(
2528 serde_json::json!({"name": "nightly", "ttl_ms": 60_000}),
2529 )
2530 .expect("the same checkpoint body without a typo decodes");
2531 assert!(serde_json::from_value::<CreateCheckpointRequest>(
2532 serde_json::json!({"name": "nightly", "ttlMs": 60_000})
2533 )
2534 .is_err());
2535
2536 serde_json::from_value::<StoreProbeRequest>(serde_json::json!({}))
2539 .expect("an empty probe body decodes");
2540 assert!(
2541 serde_json::from_value::<StoreProbeRequest>(serde_json::json!({"deep": true})).is_err()
2542 );
2543
2544 serde_json::from_value::<CreateNamespaceRequest>(serde_json::json!({
2545 "namespace_id": "demo",
2546 }))
2547 .expect("the same create body without a typo decodes");
2548 assert!(
2549 serde_json::from_value::<CreateNamespaceRequest>(serde_json::json!({
2550 "namespace_id": "demo",
2551 "fork_of": "other"
2552 }))
2553 .is_err()
2554 );
2555 assert!(
2556 serde_json::from_value::<ForkNamespaceRequest>(serde_json::json!({
2557 "new_namespace_id": "demo",
2558 "source_namespace_id": "other"
2559 }))
2560 .is_err()
2561 );
2562 }
2563 #[test]
2564 fn update_access_round_trips_and_requires_boundary_and_grants() {
2565 let operation = FilesystemOperation::UpdateAccess {
2566 path: AbsolutePath::parse("/docs/secret").expect("path"),
2567 boundary: true,
2568 grants: serde_json::from_value(serde_json::json!({"prn_ada": ["read", "write"]}))
2569 .expect("grants"),
2570 expected_inode_id: Some(InodeId(9)),
2571 expected_access_revision_no: Some(AccessRevisionNo(2)),
2572 };
2573 let json = serde_json::json!({
2574 "kind": "update_access",
2575 "path": "/docs/secret",
2576 "boundary": true,
2577 "grants": {"prn_ada": ["read", "write"]},
2578 "expected_inode_id": "ino_9",
2579 "expected_access_revision_no": 2
2580 });
2581 assert_eq!(serde_json::to_value(&operation).expect("serialize"), json);
2582 assert_eq!(
2583 serde_json::from_value::<FilesystemOperation>(json.clone()).expect("decode"),
2584 operation
2585 );
2586 for field in ["boundary", "grants"] {
2587 let mut missing = json.clone();
2588 missing.as_object_mut().expect("object").remove(field);
2589 assert!(
2590 serde_json::from_value::<FilesystemOperation>(missing).is_err(),
2591 "missing {field}"
2592 );
2593 }
2594 assert_eq!(
2595 serde_json::to_value(CommitPrecondition::AccessRevision {
2596 inode_id: InodeId(9),
2597 expected_access_revision_no: AccessRevisionNo(2),
2598 })
2599 .expect("serialize precondition"),
2600 serde_json::json!({"kind": "access_revision", "inode_id": "ino_9", "expected_access_revision_no": 2})
2601 );
2602 }
2603}