1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
//! The clap argument grammar for every `loonfs` command.
use crate::progress::ProgressMode;
use clap::{Args, Parser, Subcommand, ValueEnum};
use std::io::IsTerminal;
use std::path::PathBuf;
/// `loonfs x.y.z (commit date)`: the string served by both `--version` and
/// the `version` subcommand, built from the metadata `build.rs` embeds.
pub(crate) const LONG_VERSION: &str = concat!(
env!("CARGO_PKG_VERSION"),
" (",
env!("LOON_GIT_COMMIT"),
" ",
env!("LOON_GIT_COMMIT_DATE"),
")"
);
#[derive(Debug, Parser)]
#[command(name = "loonfs", version = LONG_VERSION)]
pub(crate) struct Cli {
/// Config file to use, ahead of LOONFS_CONFIG and the default location.
#[arg(long, global = true, value_name = "PATH")]
pub config: Option<PathBuf>,
/// Emit machine-readable JSON instead of human output.
#[arg(long, global = true)]
pub json: bool,
/// Never prompt; fail instead when input would be required.
#[arg(long, global = true)]
pub no_input: bool,
/// Say nothing about a transfer while it runs.
#[arg(long, global = true)]
pub no_progress: bool,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub(crate) enum Command {
/// Create the config file and a first profile.
Init(InitArgs),
/// Manage connection profiles (embedded stores and remote servers).
Profile {
#[command(subcommand)]
command: ProfileCommand,
},
/// Create, fork, or delete namespaces.
Namespace {
#[command(subcommand)]
command: NamespaceCommand,
},
/// Set the default namespace for a profile.
Use(NamespaceUseArgs),
/// Show the active profile and its default namespace.
Current(CurrentArgs),
/// List a directory.
Ls(FilesystemLsArgs),
/// Describe one path (kind, size, revision, content digest).
Stat(FilesystemPathArgs),
/// Print a file's content to stdout.
Cat(FilesystemCatArgs),
/// Search file content through the grep index.
Grep(FilesystemGrepArgs),
/// Download a file (or directory tree with -r) to a local path.
Get(FilesystemGetArgs),
/// Upload a local file (or directory tree with -r) to a namespace path.
Put(FilesystemPutArgs),
/// List a file's revision history, newest first.
Revisions(FilesystemRevisionsArgs),
/// Write a prior revision's content as the file's next revision.
Restore(FilesystemRestoreArgs),
/// Recover a deleted file or directory at a destination path.
Undelete(FilesystemUndeleteArgs),
/// Create a directory.
Mkdir(FilesystemMkdirArgs),
/// Delete a file or directory.
Rm(FilesystemRmArgs),
/// Move or rename a path.
Mv(FilesystemTransferArgs),
/// Copy a file (or directory tree with -r) to another path.
Cp(FilesystemTransferArgs),
/// List recoverable deletions: what was deleted, when, and the exact
/// handle `undelete` needs.
Trash(TrashArgs),
/// List committed changes after a sequence number.
Changes(ChangesArgs),
/// Maintenance operations: checkpoints, steps, retention, GC, indexes.
Admin {
#[command(subcommand)]
command: AdminCommand,
},
/// Inspect the CLI config file.
Config {
#[command(subcommand)]
command: ConfigCommand,
},
/// Print version and build metadata.
Version,
}
#[derive(Debug, Args)]
pub(crate) struct InitArgs {
pub name: Option<String>,
/// Profile mode to configure.
#[arg(long, value_name = "embedded|remote")]
pub mode: Option<String>,
/// Embedded object-store provider.
#[arg(long)]
pub store_kind: Option<String>,
/// Local filesystem store root.
#[arg(long)]
pub root: Option<String>,
/// Optional object-key prefix within the provider.
#[arg(long)]
pub key_prefix: Option<String>,
/// S3, R2, or GCS bucket name.
#[arg(long)]
pub bucket: Option<String>,
/// AWS region.
#[arg(long)]
pub region: Option<String>,
// Provider secrets fall back to the standard environment variables, but
// not through clap's `env`: a value clap filled from the environment is
// indistinguishable from one the caller typed, which turns an ambient
// AWS key into a flag "passed" to a GCS profile. The fallback is applied
// where the value is consumed instead — see `AmbientCredentials`.
/// AWS or R2 access key id (env AWS_ACCESS_KEY_ID).
#[arg(long)]
pub access_key_id: Option<String>,
/// AWS or R2 secret access key (env AWS_SECRET_ACCESS_KEY).
#[arg(long)]
pub secret_access_key: Option<String>,
/// Custom provider endpoint URL.
#[arg(long)]
pub endpoint_url: Option<String>,
/// Optional AWS session token (env AWS_SESSION_TOKEN).
#[arg(long)]
pub session_token: Option<String>,
/// Use path-style S3 addressing.
#[arg(long)]
pub force_path_style: bool,
/// Cloudflare R2 account id.
#[arg(long)]
pub account_id: Option<String>,
/// Azure storage account name.
#[arg(long)]
pub account_name: Option<String>,
/// Azure blob container name.
#[arg(long)]
pub container_name: Option<String>,
/// Azure storage access key.
#[arg(long)]
pub access_key: Option<String>,
/// Path to a GCP service-account key.
#[arg(long)]
pub service_account_key_path: Option<String>,
/// Remote LoonFS server URL.
#[arg(long)]
pub server_url: Option<String>,
/// Remote LoonFS bearer token (env LOONFS_AUTH_TOKEN).
#[arg(long)]
pub auth_token: Option<String>,
/// PEM bundle of extra certificate authorities to trust for an
/// https server URL, when a private CA issued the certificate.
#[arg(long)]
pub ca_cert_path: Option<String>,
}
#[derive(Debug, Subcommand)]
pub(crate) enum ProfileCommand {
/// Add a profile to the config file.
Create(ProfileCreateArgs),
/// List configured profiles.
List,
/// Show one profile (secrets redacted).
Show { name: Option<String> },
/// Update fields of an existing profile.
Update(ProfileUpdateArgs),
/// Delete a profile from the config file.
Delete { name: String },
/// Make a profile the default.
Use { name: String },
}
#[derive(Debug, Args)]
pub(crate) struct ProfileCreateArgs {
pub name: String,
/// Profile mode to configure.
#[arg(long, value_name = "embedded|remote")]
pub mode: Option<String>,
/// Embedded object-store provider.
#[arg(long)]
pub store_kind: Option<String>,
/// Local filesystem store root.
#[arg(long)]
pub root: Option<String>,
/// Optional object-key prefix within the provider.
#[arg(long)]
pub key_prefix: Option<String>,
/// S3, R2, or GCS bucket name.
#[arg(long)]
pub bucket: Option<String>,
/// AWS region.
#[arg(long)]
pub region: Option<String>,
// Same environment fallbacks as `InitArgs`, and for the same reason.
/// AWS or R2 access key id (env AWS_ACCESS_KEY_ID).
#[arg(long)]
pub access_key_id: Option<String>,
/// AWS or R2 secret access key (env AWS_SECRET_ACCESS_KEY).
#[arg(long)]
pub secret_access_key: Option<String>,
/// Custom provider endpoint URL.
#[arg(long)]
pub endpoint_url: Option<String>,
/// Optional AWS session token (env AWS_SESSION_TOKEN).
#[arg(long)]
pub session_token: Option<String>,
/// Use path-style S3 addressing.
#[arg(long)]
pub force_path_style: bool,
/// Cloudflare R2 account id.
#[arg(long)]
pub account_id: Option<String>,
/// Azure storage account name.
#[arg(long)]
pub account_name: Option<String>,
/// Azure blob container name.
#[arg(long)]
pub container_name: Option<String>,
/// Azure storage access key.
#[arg(long)]
pub access_key: Option<String>,
/// Path to a GCP service-account key.
#[arg(long)]
pub service_account_key_path: Option<String>,
/// Remote LoonFS server URL.
#[arg(long)]
pub server_url: Option<String>,
/// Remote LoonFS bearer token (env LOONFS_AUTH_TOKEN).
#[arg(long)]
pub auth_token: Option<String>,
/// PEM bundle of extra certificate authorities to trust for an
/// https server URL, when a private CA issued the certificate.
#[arg(long)]
pub ca_cert_path: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct ProfileUpdateArgs {
pub name: String,
/// Local filesystem store root.
#[arg(long)]
pub root: Option<String>,
/// Optional object-key prefix within the provider.
#[arg(long)]
pub key_prefix: Option<String>,
/// S3, R2, or GCS bucket name.
#[arg(long)]
pub bucket: Option<String>,
/// AWS region.
#[arg(long)]
pub region: Option<String>,
/// AWS or R2 access key id.
#[arg(long)]
pub access_key_id: Option<String>,
/// AWS or R2 secret access key.
#[arg(long)]
pub secret_access_key: Option<String>,
/// Custom provider endpoint URL.
#[arg(long)]
pub endpoint_url: Option<String>,
/// Optional AWS session token.
#[arg(long)]
pub session_token: Option<String>,
/// Cloudflare R2 account id.
#[arg(long)]
pub account_id: Option<String>,
/// Azure storage account name.
#[arg(long)]
pub account_name: Option<String>,
/// Azure blob container name.
#[arg(long)]
pub container_name: Option<String>,
/// Azure storage access key.
#[arg(long)]
pub access_key: Option<String>,
/// Path to a GCP service-account key.
#[arg(long)]
pub service_account_key_path: Option<String>,
/// Remote LoonFS server URL.
#[arg(long)]
pub server_url: Option<String>,
/// Remote LoonFS bearer token.
#[arg(long)]
pub auth_token: Option<String>,
/// PEM bundle of extra certificate authorities to trust for an
/// https server URL, when a private CA issued the certificate.
#[arg(long)]
pub ca_cert_path: Option<String>,
}
#[derive(Debug, Args, Clone)]
pub(crate) struct ProfileSelectorArgs {
/// Profile to run against (defaults to the configured default profile).
#[arg(long)]
pub profile: Option<String>,
/// Disable bounded retry of `server_busy`, `commit_queue_full`,
/// `shutting_down`, and transport errors.
#[arg(long)]
pub no_retry: bool,
}
#[derive(Debug, Args, Clone)]
pub(crate) struct TargetSelectorArgs {
#[command(flatten)]
pub profile: ProfileSelectorArgs,
#[arg(long)]
pub namespace: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct NamespaceUseArgs {
#[command(flatten)]
pub profile: ProfileSelectorArgs,
pub namespace: String,
}
#[derive(Debug, Args)]
pub(crate) struct CurrentArgs {
#[command(flatten)]
pub profile: ProfileSelectorArgs,
}
#[derive(Debug, Subcommand)]
pub(crate) enum NamespaceCommand {
/// Create a new empty namespace.
Create(NamespaceCreateArgs),
/// Permanently delete a namespace and retire its id.
Delete(NamespaceDeleteArgs),
/// Fork a namespace into a new one; O(1), no bytes copied.
Fork(NamespaceForkArgs),
}
#[derive(Debug, Args)]
pub(crate) struct NamespaceCreateArgs {
#[command(flatten)]
pub profile: ProfileSelectorArgs,
pub namespace_id: String,
}
#[derive(Debug, Args)]
pub(crate) struct NamespaceDeleteArgs {
#[command(flatten)]
pub profile: ProfileSelectorArgs,
pub namespace_id: String,
/// Delete only if the namespace head is still at this sequence.
#[arg(long)]
pub expected_head_seq: Option<u64>,
/// Skip the interactive confirmation.
#[arg(long)]
pub yes: bool,
}
#[derive(Debug, Args)]
pub(crate) struct NamespaceForkArgs {
#[command(flatten)]
pub profile: ProfileSelectorArgs,
pub source: String,
pub new_namespace_id: String,
}
#[derive(Debug, Args)]
pub(crate) struct FilesystemLsArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
pub path: Option<String>,
/// Stop after this many entries in total. Without it the command
/// follows cursors and prints the whole directory, however large.
#[arg(long)]
pub limit: Option<u32>,
/// Resume cursor from a previous bounded listing.
#[arg(long)]
pub cursor: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct FilesystemPathArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
pub path: String,
}
#[derive(Debug, Args)]
pub(crate) struct FilesystemRmArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
pub path: String,
/// Delete a directory and everything under it, as one commit. The whole
/// subtree stays recoverable through the printed undelete handle.
#[arg(short, long)]
pub recursive: bool,
/// Annotation recorded on the commit and shown by `loonfs changes`. Part
/// of the commit's identity: resubmitting the same --commit-id with a
/// different message is a commit id conflict.
#[arg(short = 'm', long)]
pub message: Option<String>,
/// Idempotency key for the commit; resubmit with the same id to retry
/// safely. Generated when absent and returned in the output.
#[arg(long)]
pub commit_id: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct FilesystemMkdirArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
pub path: String,
/// Create missing parent directories as well.
#[arg(short = 'p', long)]
pub parents: bool,
/// Annotation recorded on the commit and shown by `loonfs changes`. Part
/// of the commit's identity: resubmitting the same --commit-id with a
/// different message is a commit id conflict.
#[arg(short = 'm', long)]
pub message: Option<String>,
/// Idempotency key for the commit; resubmit with the same id to retry
/// safely. Generated when absent and returned in the output.
#[arg(long)]
pub commit_id: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct FilesystemRevisionsArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
pub path: String,
/// Maximum revisions to return in this page.
#[arg(long)]
pub limit: Option<u32>,
/// Resume cursor from a previous revisions page.
#[arg(long)]
pub cursor: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct FilesystemCatArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
pub path: String,
/// Print this revision instead of the current content.
#[arg(long)]
pub revision: Option<u64>,
}
#[derive(Debug, Args)]
pub(crate) struct FilesystemGrepArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
/// Pattern in the Rust regex dialect; `^`/`$` anchor lines.
pub pattern: String,
/// Restrict matches to files under this absolute path prefix.
#[arg(long)]
pub path_prefix: Option<String>,
/// Match ASCII letters without regard to case.
#[arg(short = 'i', long)]
pub ignore_case: bool,
/// Matches fetched per page, bounded by the deployment's
/// `query.grep.max_limit`. To bound the total, use --max-matches.
#[arg(long)]
pub limit: Option<u32>,
/// Stop after this many matches in total. Without it the command
/// follows cursors to completion and prints every match.
#[arg(long)]
pub max_matches: Option<u32>,
/// Permit a capped exhaustive scan for patterns with no literal bytes.
#[arg(long)]
pub allow_scan: bool,
/// Accept indexed-only results when the unindexed tail exceeds the
/// scan budget.
#[arg(long)]
pub allow_stale: bool,
}
#[derive(Debug, Args)]
pub(crate) struct FilesystemGetArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
pub remote_path: String,
/// Local destination (defaults to the remote basename; `-` streams to
/// stdout). A large file is written as it arrives and never held whole,
/// so what a get costs in memory does not follow what it downloads. A
/// file destination is written beside itself and renamed into place only
/// once the download is complete and its content verified, so a failed
/// download leaves nothing there. Streaming to stdout hands bytes on as
/// they arrive, so content that fails verification at the end exits
/// nonzero after part of it has already been written — the exit status,
/// not the output, is what says the content was verified.
pub local_destination: Option<String>,
/// Download the directory tree rooted at `remote_path`, with bounded
/// concurrency and per-file outcomes. The local destination is created
/// if it does not exist.
#[arg(short, long)]
pub recursive: bool,
/// Download this revision instead of the current content.
#[arg(long)]
pub revision: Option<u64>,
/// Overwrite the local destination if it already exists.
#[arg(long)]
pub force: bool,
}
#[derive(Debug, Args)]
pub(crate) struct FilesystemPutArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
/// Local file to upload, or `-` to read standard input. A large file
/// and a pipe are both read once and never held whole, so what a put
/// costs in memory does not follow what it uploads. Reading `-` needs
/// an explicit remote path.
pub local_path: String,
pub remote_path: Option<String>,
/// Upload the directory tree rooted at `local_path`. Every file lands
/// as its own commit with bounded concurrency, so progress is per file
/// and a partial failure reruns per file.
#[arg(short, long)]
pub recursive: bool,
/// Replace the remote destination if it already exists.
#[arg(long)]
pub force: bool,
/// Replace only while the file's current revision is still this one
/// (implies --force); a raced write fails instead of stacking on it.
#[arg(long)]
pub expected_revision: Option<u64>,
/// Annotation recorded on the commit and shown by `loonfs changes`. Part
/// of the commit's identity: resubmitting the same --commit-id with a
/// different message is a commit id conflict.
#[arg(short = 'm', long)]
pub message: Option<String>,
/// Idempotency key for the commit; resubmit with the same id to retry
/// safely. Generated when absent and returned in the output.
#[arg(long)]
pub commit_id: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct FilesystemTransferArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
pub source_path: String,
pub destination_path: String,
/// Copy the directory tree rooted at `source_path` (cp only; mv moves
/// a directory in one commit without -r).
#[arg(short, long)]
pub recursive: bool,
/// Replace the destination if it already exists.
#[arg(long)]
pub force: bool,
/// Annotation recorded on the commit and shown by `loonfs changes`. Part
/// of the commit's identity: resubmitting the same --commit-id with a
/// different message is a commit id conflict.
#[arg(short = 'm', long)]
pub message: Option<String>,
/// Idempotency key for the commit; resubmit with the same id to retry
/// safely. Generated when absent and returned in the output.
#[arg(long)]
pub commit_id: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct FilesystemRestoreArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
pub path: String,
#[arg(long)]
pub revision: u64,
/// Annotation recorded on the commit and shown by `loonfs changes`. Part
/// of the commit's identity: resubmitting the same --commit-id with a
/// different message is a commit id conflict.
#[arg(short = 'm', long)]
pub message: Option<String>,
/// Idempotency key for the commit; resubmit with the same id to retry
/// safely. Generated when absent and returned in the output.
#[arg(long)]
pub commit_id: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct FilesystemUndeleteArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
/// Destination path for the recovered file or directory. Omit to
/// restore in place: the entry re-binds under the parent and name its
/// deletion recorded, which lands correctly even when the enclosing
/// directories were renamed since. A deletion that recorded no binding
/// needs the explicit path.
pub path: Option<String>,
/// Inode id of the deleted item, as reported by `rm` and the change
/// feed.
#[arg(long)]
pub inode: u64,
/// Committed sequence of the delete being recovered, as reported by
/// `rm` and the change feed. Scopes recovery to that exact deletion,
/// so a stale command cannot cancel a later delete.
#[arg(long)]
pub deleted_at: u64,
/// Annotation recorded on the commit and shown by `loonfs changes`. Part
/// of the commit's identity: resubmitting the same --commit-id with a
/// different message is a commit id conflict.
#[arg(short = 'm', long)]
pub message: Option<String>,
/// Idempotency key for the commit; resubmit with the same id to retry
/// safely. Generated when absent and returned in the output.
#[arg(long)]
pub commit_id: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct TrashArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
/// Maximum number of entries to return.
#[arg(long)]
pub limit: Option<u32>,
/// Resume cursor from a previous page.
#[arg(long)]
pub cursor: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct ChangesArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
/// Return committed changes after this sequence (defaults to 0, the
/// start of retained history).
#[arg(long)]
pub after: Option<u64>,
/// Maximum number of changes to return.
#[arg(long)]
pub limit: Option<u32>,
}
#[derive(Debug, Subcommand)]
pub(crate) enum AdminCommand {
/// Pin the namespace's current state under a named checkpoint.
Checkpoint(AdminCheckpointArgs),
/// List the namespace's active checkpoint pins, oldest first. A
/// checkpoint name is a label, not a key, so this is how a pin is found
/// again when its id has been lost.
CheckpointList(AdminNamespaceArgs),
/// Release a checkpoint pin.
CheckpointRelease(AdminCheckpointReleaseArgs),
/// Flush the WAL tail into a durable segment.
Flush(AdminNamespaceArgs),
/// Advance the retention floor, surrendering replay history below the
/// flushed manifest head. File revision history is never affected.
RetentionAdvance(AdminNamespaceArgs),
/// Host maintenance for explicitly assigned namespaces: continuously
/// until a signal, or as one bounded catch-up with --drain.
Run(AdminRunArgs),
/// Run one core maintenance step (WAL flush and metadata folds).
Step(AdminStepArgs),
/// Run a mark-and-sweep garbage-collection pass.
Gc(AdminGcArgs),
/// Prove the profile's object store honours the contract LoonFS
/// depends on, and report what it found check by check.
ProbeStore(AdminProbeStoreArgs),
/// Enable the gram content index and wait for its backfill to reach the
/// sequence the namespace was at when this command started.
IndexEnable(AdminIndexEnableArgs),
/// Disable the gram content index.
IndexDisable(AdminNamespaceArgs),
/// Report where the gram content index is: disabled, backfilling, or
/// steady at a watermark.
IndexStatus(AdminNamespaceArgs),
/// Collect the namespace's unreferenced gram-index objects.
IndexGc(AdminIndexGcArgs),
}
/// Floor on `--poll-interval-ms`. A re-assertion is a nudge per assigned
/// key and the runner answers each one by reading durable state, so a
/// cadence below this buys nothing and only spends provider requests.
const MIN_POLL_INTERVAL_MS: u64 = 100;
#[derive(Debug, Args)]
pub(crate) struct AdminRunArgs {
#[command(flatten)]
pub profile: ProfileSelectorArgs,
/// Namespace this host maintains. Repeat the flag to assign more; at
/// least one is required, because this command never discovers
/// namespaces.
#[arg(long = "namespace", required = true)]
pub namespaces: Vec<String>,
/// Maintenance job to host. Repeat the flag to select more; all three
/// when omitted. `core-gc` selects the runtime's collection job, which
/// logs and settles under its own name, `gc`.
#[arg(long = "job")]
pub jobs: Vec<MaintenanceJobArg>,
/// How often every assigned namespace is re-checked for work, in
/// milliseconds. Defaults to 60000, and a drain ignores it because it
/// never rests between keys.
#[arg(long, value_parser = clap::value_parser!(u64).range(MIN_POLL_INTERVAL_MS..))]
pub poll_interval_ms: Option<u64>,
/// Catch every assigned namespace up and exit, instead of hosting until
/// a signal.
#[arg(long)]
pub drain: bool,
/// Give up after this many steps across the whole drain. Exits nonzero
/// and reports where every key got to. Requires --drain.
#[arg(long, requires = "drain")]
pub max_steps: Option<u64>,
/// Give up after this many milliseconds across the whole drain. Exits
/// nonzero and reports where every key got to. Requires --drain.
#[arg(long, requires = "drain")]
pub deadline_ms: Option<u64>,
}
/// The maintenance jobs `admin run` can host, as an operator names them.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum MaintenanceJobArg {
/// Flush the WAL tail past its threshold and fold one reorganization
/// unit per step.
Metadata,
/// Run one bounded mark-and-sweep collection pass per step.
CoreGc,
/// Build and fold the gram content index.
GrepIndex,
/// Reclaim one namespace's unreferenced grep objects per step.
GrepGc,
}
#[derive(Debug, Args)]
pub(crate) struct AdminIndexEnableArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
/// Return as soon as the index is enabled, without waiting for the
/// backfill.
#[arg(long)]
pub no_wait: bool,
/// Give up after this many steps: one bounded index step where the
/// profile is embedded, one status check where it is remote. Exits
/// nonzero and reports how far the index got.
#[arg(long)]
pub max_steps: Option<u64>,
/// Give up after this many milliseconds. Exits nonzero and reports how
/// far the index got.
#[arg(long)]
pub deadline_ms: Option<u64>,
}
#[derive(Debug, Args)]
pub(crate) struct AdminIndexGcArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
/// Spend at most this many reads and return after one bounded pass.
/// Omit to loop bounded passes through completion.
#[arg(long)]
pub max_objects: Option<u64>,
}
#[derive(Debug, Args)]
pub(crate) struct AdminNamespaceArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
}
#[derive(Debug, Args)]
pub(crate) struct AdminCheckpointArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
/// Label recorded on the checkpoint record (a label, not a key).
#[arg(long)]
pub name: String,
/// Optional lifetime; the record expires this many milliseconds from
/// now. Omitted means the pin holds until explicitly released.
#[arg(long)]
pub ttl_ms: Option<u64>,
}
#[derive(Debug, Args)]
pub(crate) struct AdminCheckpointReleaseArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
/// Checkpoint id to release.
pub checkpoint_id: String,
}
#[derive(Debug, Args)]
pub(crate) struct AdminStepArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
/// Flush the visible WAL tail into metadata tables when it reaches this many
/// segments (server default when omitted).
#[arg(long)]
pub max_wal_tail_segments: Option<u64>,
/// Advance the retention floor after the step's flush work. Replay
/// history below the flushed manifest head is surrendered.
#[arg(long)]
pub retention: bool,
/// Run a garbage-collection pass after the step's flush work.
#[arg(long)]
pub gc: bool,
}
#[derive(Debug, Args)]
pub(crate) struct AdminGcArgs {
#[command(flatten)]
pub target: TargetSelectorArgs,
/// Objects younger than this are never deleted (server default when
/// omitted).
#[arg(long)]
pub grace_window_ms: Option<u64>,
/// Examine at most this many candidates and return after one bounded
/// pass. Omit to loop bounded passes through completion.
#[arg(long)]
pub max_objects: Option<u64>,
}
#[derive(Debug, Args)]
pub(crate) struct AdminProbeStoreArgs {
#[command(flatten)]
pub profile: ProfileSelectorArgs,
}
#[derive(Debug, Subcommand)]
pub(crate) enum ConfigCommand {
/// Print the config file path.
Path,
/// Print the config file (secrets redacted).
Show,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RuntimeBehavior {
pub json: bool,
pub no_input: bool,
pub interactive: bool,
/// How a transfer that takes human time says where it has got to.
pub progress: ProgressMode,
}
impl RuntimeBehavior {
pub(crate) fn detect(cli: &Cli) -> Self {
let interactive = !cli.json
&& !cli.no_input
&& std::io::stdin().is_terminal()
&& std::io::stderr().is_terminal();
Self {
json: cli.json,
no_input: cli.no_input,
interactive,
// Progress asks standard error alone, not the terminal pair
// `interactive` needs: a `put` fed by a pipe still has a
// terminal to draw on, and `--no-input` says nothing about
// whether anyone is watching.
progress: ProgressMode::detect(cli.no_progress, cli.json),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CommandKind {
Init,
ProfileCreate,
ProfileList,
ProfileShow,
ProfileUpdate,
ProfileDelete,
ProfileUse,
NamespaceCreate,
NamespaceDelete,
NamespaceFork,
NamespaceUse,
Current,
FilesystemLs,
FilesystemStat,
FilesystemCat,
FilesystemGrep,
FilesystemGet,
FilesystemPut,
FilesystemRevisions,
FilesystemTrash,
FilesystemRestore,
FilesystemUndelete,
FilesystemMkdir,
FilesystemRm,
FilesystemMv,
FilesystemCp,
Changes,
AdminCheckpoint,
AdminCheckpointList,
AdminCheckpointRelease,
AdminFlush,
AdminRetentionAdvance,
AdminRun,
AdminStep,
AdminGc,
AdminProbeStore,
AdminIndexEnable,
AdminIndexDisable,
AdminIndexStatus,
AdminIndexGc,
ConfigPath,
ConfigShow,
Version,
}
impl CommandKind {
pub(crate) fn as_str(self) -> &'static str {
match self {
CommandKind::Init => "init",
CommandKind::ProfileCreate => "profile_create",
CommandKind::ProfileList => "profile_list",
CommandKind::ProfileShow => "profile_show",
CommandKind::ProfileUpdate => "profile_update",
CommandKind::ProfileDelete => "profile_delete",
CommandKind::ProfileUse => "profile_use",
CommandKind::NamespaceCreate => "namespace_create",
CommandKind::NamespaceDelete => "namespace_delete",
CommandKind::NamespaceFork => "namespace_fork",
CommandKind::NamespaceUse => "namespace_use",
CommandKind::Current => "current",
CommandKind::FilesystemLs => "filesystem_ls",
CommandKind::FilesystemStat => "filesystem_stat",
CommandKind::FilesystemCat => "filesystem_cat",
CommandKind::FilesystemGrep => "filesystem_grep",
CommandKind::FilesystemGet => "filesystem_get",
CommandKind::FilesystemPut => "filesystem_put",
CommandKind::FilesystemRevisions => "filesystem_revisions",
CommandKind::FilesystemTrash => "filesystem_trash",
CommandKind::FilesystemRestore => "filesystem_restore",
CommandKind::FilesystemUndelete => "filesystem_undelete",
CommandKind::FilesystemMkdir => "filesystem_mkdir",
CommandKind::FilesystemRm => "filesystem_rm",
CommandKind::FilesystemMv => "filesystem_mv",
CommandKind::FilesystemCp => "filesystem_cp",
CommandKind::Changes => "changes",
CommandKind::AdminCheckpoint => "admin_checkpoint",
CommandKind::AdminCheckpointList => "admin_checkpoint_list",
CommandKind::AdminCheckpointRelease => "admin_checkpoint_release",
CommandKind::AdminFlush => "admin_flush",
CommandKind::AdminRetentionAdvance => "admin_retention_advance",
CommandKind::AdminRun => "admin_run",
CommandKind::AdminStep => "admin_step",
CommandKind::AdminGc => "admin_gc",
CommandKind::AdminProbeStore => "admin_probe_store",
CommandKind::AdminIndexEnable => "admin_index_enable",
CommandKind::AdminIndexDisable => "admin_index_disable",
CommandKind::AdminIndexStatus => "admin_index_status",
CommandKind::AdminIndexGc => "admin_index_gc",
CommandKind::ConfigPath => "config_path",
CommandKind::ConfigShow => "config_show",
CommandKind::Version => "version",
}
}
pub(crate) fn supports_json(self) -> bool {
!matches!(self, CommandKind::FilesystemCat)
}
}
impl Cli {
pub(crate) fn kind(&self) -> CommandKind {
match &self.command {
Command::Init(_) => CommandKind::Init,
Command::Profile { command } => match command {
ProfileCommand::Create(_) => CommandKind::ProfileCreate,
ProfileCommand::List => CommandKind::ProfileList,
ProfileCommand::Show { .. } => CommandKind::ProfileShow,
ProfileCommand::Update(_) => CommandKind::ProfileUpdate,
ProfileCommand::Delete { .. } => CommandKind::ProfileDelete,
ProfileCommand::Use { .. } => CommandKind::ProfileUse,
},
Command::Namespace { command } => match command {
NamespaceCommand::Create(_) => CommandKind::NamespaceCreate,
NamespaceCommand::Delete(_) => CommandKind::NamespaceDelete,
NamespaceCommand::Fork(_) => CommandKind::NamespaceFork,
},
Command::Use(_) => CommandKind::NamespaceUse,
Command::Current(_) => CommandKind::Current,
Command::Ls(_) => CommandKind::FilesystemLs,
Command::Stat(_) => CommandKind::FilesystemStat,
Command::Cat(_) => CommandKind::FilesystemCat,
Command::Grep(_) => CommandKind::FilesystemGrep,
Command::Get(_) => CommandKind::FilesystemGet,
Command::Put(_) => CommandKind::FilesystemPut,
Command::Revisions(_) => CommandKind::FilesystemRevisions,
Command::Trash(_) => CommandKind::FilesystemTrash,
Command::Restore(_) => CommandKind::FilesystemRestore,
Command::Undelete(_) => CommandKind::FilesystemUndelete,
Command::Mkdir(_) => CommandKind::FilesystemMkdir,
Command::Rm(_) => CommandKind::FilesystemRm,
Command::Mv(_) => CommandKind::FilesystemMv,
Command::Cp(_) => CommandKind::FilesystemCp,
Command::Changes(_) => CommandKind::Changes,
Command::Admin { command } => match command {
AdminCommand::Checkpoint(_) => CommandKind::AdminCheckpoint,
AdminCommand::CheckpointList(_) => CommandKind::AdminCheckpointList,
AdminCommand::CheckpointRelease(_) => CommandKind::AdminCheckpointRelease,
AdminCommand::Flush(_) => CommandKind::AdminFlush,
AdminCommand::RetentionAdvance(_) => CommandKind::AdminRetentionAdvance,
AdminCommand::Run(_) => CommandKind::AdminRun,
AdminCommand::Step(_) => CommandKind::AdminStep,
AdminCommand::Gc(_) => CommandKind::AdminGc,
AdminCommand::ProbeStore(_) => CommandKind::AdminProbeStore,
AdminCommand::IndexEnable(_) => CommandKind::AdminIndexEnable,
AdminCommand::IndexDisable(_) => CommandKind::AdminIndexDisable,
AdminCommand::IndexStatus(_) => CommandKind::AdminIndexStatus,
AdminCommand::IndexGc(_) => CommandKind::AdminIndexGc,
},
Command::Config { command } => match command {
ConfigCommand::Path => CommandKind::ConfigPath,
ConfigCommand::Show => CommandKind::ConfigShow,
},
Command::Version => CommandKind::Version,
}
}
}