summaryrefslogtreecommitdiff
path: root/fctdrive
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2025-08-08 17:40:19 +0100
committerdiogo464 <[email protected]>2025-08-08 17:40:44 +0100
commitbebf1cd5fc4668b7970e3e2e426ad103ecbc670c (patch)
treeafee1e59890836e367a075749745066450d63f44 /fctdrive
parent9a25abd1d6ef6f5b0e2c08751183f63db43c73a5 (diff)
rust cli init
Diffstat (limited to 'fctdrive')
-rw-r--r--fctdrive/Cargo.lock351
-rw-r--r--fctdrive/Cargo.toml13
-rw-r--r--fctdrive/src/main.rs1025
3 files changed, 1389 insertions, 0 deletions
diff --git a/fctdrive/Cargo.lock b/fctdrive/Cargo.lock
new file mode 100644
index 0000000..9642104
--- /dev/null
+++ b/fctdrive/Cargo.lock
@@ -0,0 +1,351 @@
1# This file is automatically @generated by Cargo.
2# It is not intended for manual editing.
3version = 4
4
5[[package]]
6name = "anstream"
7version = "0.6.20"
8source = "registry+https://github.com/rust-lang/crates.io-index"
9checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
10dependencies = [
11 "anstyle",
12 "anstyle-parse",
13 "anstyle-query",
14 "anstyle-wincon",
15 "colorchoice",
16 "is_terminal_polyfill",
17 "utf8parse",
18]
19
20[[package]]
21name = "anstyle"
22version = "1.0.11"
23source = "registry+https://github.com/rust-lang/crates.io-index"
24checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
25
26[[package]]
27name = "anstyle-parse"
28version = "0.2.7"
29source = "registry+https://github.com/rust-lang/crates.io-index"
30checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
31dependencies = [
32 "utf8parse",
33]
34
35[[package]]
36name = "anstyle-query"
37version = "1.1.4"
38source = "registry+https://github.com/rust-lang/crates.io-index"
39checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
40dependencies = [
41 "windows-sys",
42]
43
44[[package]]
45name = "anstyle-wincon"
46version = "3.0.10"
47source = "registry+https://github.com/rust-lang/crates.io-index"
48checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
49dependencies = [
50 "anstyle",
51 "once_cell_polyfill",
52 "windows-sys",
53]
54
55[[package]]
56name = "block-buffer"
57version = "0.10.4"
58source = "registry+https://github.com/rust-lang/crates.io-index"
59checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
60dependencies = [
61 "generic-array",
62]
63
64[[package]]
65name = "cfg-if"
66version = "1.0.1"
67source = "registry+https://github.com/rust-lang/crates.io-index"
68checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
69
70[[package]]
71name = "clap"
72version = "4.5.43"
73source = "registry+https://github.com/rust-lang/crates.io-index"
74checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f"
75dependencies = [
76 "clap_builder",
77 "clap_derive",
78]
79
80[[package]]
81name = "clap_builder"
82version = "4.5.43"
83source = "registry+https://github.com/rust-lang/crates.io-index"
84checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65"
85dependencies = [
86 "anstream",
87 "anstyle",
88 "clap_lex",
89 "strsim",
90]
91
92[[package]]
93name = "clap_derive"
94version = "4.5.41"
95source = "registry+https://github.com/rust-lang/crates.io-index"
96checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491"
97dependencies = [
98 "heck",
99 "proc-macro2",
100 "quote",
101 "syn",
102]
103
104[[package]]
105name = "clap_lex"
106version = "0.7.5"
107source = "registry+https://github.com/rust-lang/crates.io-index"
108checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
109
110[[package]]
111name = "colorchoice"
112version = "1.0.4"
113source = "registry+https://github.com/rust-lang/crates.io-index"
114checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
115
116[[package]]
117name = "cpufeatures"
118version = "0.2.17"
119source = "registry+https://github.com/rust-lang/crates.io-index"
120checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
121dependencies = [
122 "libc",
123]
124
125[[package]]
126name = "crypto-common"
127version = "0.1.6"
128source = "registry+https://github.com/rust-lang/crates.io-index"
129checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
130dependencies = [
131 "generic-array",
132 "typenum",
133]
134
135[[package]]
136name = "digest"
137version = "0.10.7"
138source = "registry+https://github.com/rust-lang/crates.io-index"
139checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
140dependencies = [
141 "block-buffer",
142 "crypto-common",
143]
144
145[[package]]
146name = "fctdrive"
147version = "0.1.0"
148dependencies = [
149 "clap",
150 "hex",
151 "sha2",
152 "slotmap",
153]
154
155[[package]]
156name = "generic-array"
157version = "0.14.7"
158source = "registry+https://github.com/rust-lang/crates.io-index"
159checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
160dependencies = [
161 "typenum",
162 "version_check",
163]
164
165[[package]]
166name = "heck"
167version = "0.5.0"
168source = "registry+https://github.com/rust-lang/crates.io-index"
169checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
170
171[[package]]
172name = "hex"
173version = "1.1.0"
174source = "git+https://git.d464.sh/hex-rs#ed091ffa8206658262a0e2409a35d8910e5d0682"
175
176[[package]]
177name = "is_terminal_polyfill"
178version = "1.70.1"
179source = "registry+https://github.com/rust-lang/crates.io-index"
180checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
181
182[[package]]
183name = "libc"
184version = "0.2.174"
185source = "registry+https://github.com/rust-lang/crates.io-index"
186checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
187
188[[package]]
189name = "once_cell_polyfill"
190version = "1.70.1"
191source = "registry+https://github.com/rust-lang/crates.io-index"
192checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
193
194[[package]]
195name = "proc-macro2"
196version = "1.0.95"
197source = "registry+https://github.com/rust-lang/crates.io-index"
198checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
199dependencies = [
200 "unicode-ident",
201]
202
203[[package]]
204name = "quote"
205version = "1.0.40"
206source = "registry+https://github.com/rust-lang/crates.io-index"
207checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
208dependencies = [
209 "proc-macro2",
210]
211
212[[package]]
213name = "sha2"
214version = "0.10.9"
215source = "registry+https://github.com/rust-lang/crates.io-index"
216checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
217dependencies = [
218 "cfg-if",
219 "cpufeatures",
220 "digest",
221]
222
223[[package]]
224name = "slotmap"
225version = "1.0.7"
226source = "registry+https://github.com/rust-lang/crates.io-index"
227checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
228dependencies = [
229 "version_check",
230]
231
232[[package]]
233name = "strsim"
234version = "0.11.1"
235source = "registry+https://github.com/rust-lang/crates.io-index"
236checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
237
238[[package]]
239name = "syn"
240version = "2.0.104"
241source = "registry+https://github.com/rust-lang/crates.io-index"
242checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
243dependencies = [
244 "proc-macro2",
245 "quote",
246 "unicode-ident",
247]
248
249[[package]]
250name = "typenum"
251version = "1.18.0"
252source = "registry+https://github.com/rust-lang/crates.io-index"
253checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
254
255[[package]]
256name = "unicode-ident"
257version = "1.0.18"
258source = "registry+https://github.com/rust-lang/crates.io-index"
259checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
260
261[[package]]
262name = "utf8parse"
263version = "0.2.2"
264source = "registry+https://github.com/rust-lang/crates.io-index"
265checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
266
267[[package]]
268name = "version_check"
269version = "0.9.5"
270source = "registry+https://github.com/rust-lang/crates.io-index"
271checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
272
273[[package]]
274name = "windows-link"
275version = "0.1.3"
276source = "registry+https://github.com/rust-lang/crates.io-index"
277checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
278
279[[package]]
280name = "windows-sys"
281version = "0.60.2"
282source = "registry+https://github.com/rust-lang/crates.io-index"
283checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
284dependencies = [
285 "windows-targets",
286]
287
288[[package]]
289name = "windows-targets"
290version = "0.53.3"
291source = "registry+https://github.com/rust-lang/crates.io-index"
292checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
293dependencies = [
294 "windows-link",
295 "windows_aarch64_gnullvm",
296 "windows_aarch64_msvc",
297 "windows_i686_gnu",
298 "windows_i686_gnullvm",
299 "windows_i686_msvc",
300 "windows_x86_64_gnu",
301 "windows_x86_64_gnullvm",
302 "windows_x86_64_msvc",
303]
304
305[[package]]
306name = "windows_aarch64_gnullvm"
307version = "0.53.0"
308source = "registry+https://github.com/rust-lang/crates.io-index"
309checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
310
311[[package]]
312name = "windows_aarch64_msvc"
313version = "0.53.0"
314source = "registry+https://github.com/rust-lang/crates.io-index"
315checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
316
317[[package]]
318name = "windows_i686_gnu"
319version = "0.53.0"
320source = "registry+https://github.com/rust-lang/crates.io-index"
321checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
322
323[[package]]
324name = "windows_i686_gnullvm"
325version = "0.53.0"
326source = "registry+https://github.com/rust-lang/crates.io-index"
327checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
328
329[[package]]
330name = "windows_i686_msvc"
331version = "0.53.0"
332source = "registry+https://github.com/rust-lang/crates.io-index"
333checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
334
335[[package]]
336name = "windows_x86_64_gnu"
337version = "0.53.0"
338source = "registry+https://github.com/rust-lang/crates.io-index"
339checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
340
341[[package]]
342name = "windows_x86_64_gnullvm"
343version = "0.53.0"
344source = "registry+https://github.com/rust-lang/crates.io-index"
345checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
346
347[[package]]
348name = "windows_x86_64_msvc"
349version = "0.53.0"
350source = "registry+https://github.com/rust-lang/crates.io-index"
351checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
diff --git a/fctdrive/Cargo.toml b/fctdrive/Cargo.toml
new file mode 100644
index 0000000..f2ffa1d
--- /dev/null
+++ b/fctdrive/Cargo.toml
@@ -0,0 +1,13 @@
1[package]
2name = "fctdrive"
3version = "0.1.0"
4edition = "2024"
5
6[dependencies]
7clap = { version = "4.5.43", features = ["derive", "env"] }
8hex = { git = "https://git.d464.sh/hex-rs", version = "1.1.0" }
9sha2 = "0.10.9"
10slotmap = "1.0.7"
11
12[profile.release]
13debug = true
diff --git a/fctdrive/src/main.rs b/fctdrive/src/main.rs
new file mode 100644
index 0000000..0f7c83c
--- /dev/null
+++ b/fctdrive/src/main.rs
@@ -0,0 +1,1025 @@
1use std::{
2 collections::{HashMap, VecDeque},
3 fmt::Display,
4 fs::File,
5 io::{BufWriter, Read as _, Write},
6 path::{Path, PathBuf},
7 str::FromStr,
8 sync::{Arc, Mutex},
9 time::SystemTime,
10};
11
12use clap::{Args, Parser, Subcommand};
13use sha2::Digest;
14use slotmap::SlotMap;
15
16const BLOB_STORE_HIERARCHY_DEPTH: usize = 4;
17
18#[derive(Debug)]
19pub struct InvalidBlobId;
20
21#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
22pub struct BlobId([u8; 32]);
23
24impl BlobId {
25 pub fn hash(&self, data: &[u8]) -> BlobId {
26 let mut hasher = sha2::Sha256::default();
27 hasher.write_all(data).unwrap();
28 Self(hasher.finalize().try_into().unwrap())
29 }
30
31 pub fn as_bytes(&self) -> &[u8; 32] {
32 &self.0
33 }
34}
35
36impl Display for BlobId {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 hex::display(&self.0).fmt(f)
39 }
40}
41
42impl FromStr for BlobId {
43 type Err = InvalidBlobId;
44
45 fn from_str(s: &str) -> Result<Self, Self::Err> {
46 let decoded = hex::decode_hex(s).map_err(|_| InvalidBlobId)?;
47 Ok(Self(decoded.try_into().map_err(|_| InvalidBlobId)?))
48 }
49}
50
51#[derive(Debug, Clone)]
52pub struct BlobStore(PathBuf);
53
54impl BlobStore {
55 pub fn new(path: impl Into<PathBuf>) -> Self {
56 Self(path.into())
57 }
58}
59
60pub fn blob_path(store: &BlobStore, blob_id: &BlobId) -> PathBuf {
61 let encoded = hex::encode_hex(blob_id.as_bytes());
62 let mut path = store.0.clone();
63 for depth in 0..BLOB_STORE_HIERARCHY_DEPTH {
64 let base_idx = depth * 2;
65 path.push(&encoded[base_idx..base_idx + 2]);
66 }
67 path.push(encoded);
68 path
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
72pub enum ImportMode {
73 Move,
74 Copy,
75 HardLink,
76}
77
78pub fn blob_import_file(
79 store: &BlobStore,
80 mode: ImportMode,
81 blob_id: &BlobId,
82 file_path: &Path,
83) -> std::io::Result<()> {
84 let blob_path = blob_path(store, blob_id);
85 if blob_path.exists() {
86 return Ok(());
87 }
88
89 if let Some(parent) = blob_path.parent() {
90 std::fs::create_dir_all(parent)?;
91 }
92 match mode {
93 ImportMode::Move => {
94 std::fs::rename(file_path, blob_path)?;
95 }
96 ImportMode::Copy => {
97 todo!()
98 }
99 ImportMode::HardLink => match std::fs::hard_link(file_path, blob_path) {
100 Ok(()) => {}
101 Err(err) => {
102 if err.kind() != std::io::ErrorKind::AlreadyExists {
103 panic!("{err}");
104 }
105 }
106 },
107 }
108 Ok(())
109}
110
111pub fn blob_hash_file(file_path: &Path) -> std::io::Result<BlobId> {
112 let mut file = std::fs::File::open(file_path)?;
113 let mut buf = vec![0u8; 1 * 1024 * 1024];
114 let mut hasher = sha2::Sha256::default();
115 loop {
116 let n = file.read(&mut buf)?;
117 if n == 0 {
118 break;
119 }
120 hasher.write_all(&buf[..n])?;
121 }
122 Ok(BlobId(hasher.finalize().try_into().unwrap()))
123}
124
125const OPERATION_KIND_CREATE_FILE: &'static str = "create_file";
126const OPERATION_KIND_CREATE_DIR: &'static str = "create_dir";
127const OPERATION_KIND_REMOVE: &'static str = "remove";
128const OPERATION_KIND_RENAME: &'static str = "rename";
129
130#[derive(Debug)]
131pub struct InvalidOperation;
132
133#[derive(Debug, Clone, PartialEq, Eq, Hash)]
134pub struct Operation {
135 pub header: OperationHeader,
136 pub data: OperationData,
137}
138
139impl FromStr for Operation {
140 type Err = InvalidOperation;
141
142 fn from_str(s: &str) -> Result<Self, Self::Err> {
143 let err = || InvalidOperation;
144 let mut iter = s.split('\t');
145
146 let timestamp_str = iter.next().ok_or_else(err)?;
147 let revision_str = iter.next().ok_or_else(err)?;
148 let email_str = iter.next().ok_or_else(err)?;
149 let kind_str = iter.next().ok_or_else(err)?;
150
151 let timestamp = timestamp_str.parse().map_err(|_| err())?;
152 let revision = revision_str.parse().map_err(|_| err())?;
153 let email = email_str.to_string();
154 let data = match kind_str {
155 OPERATION_KIND_CREATE_FILE => {
156 let path_str = iter.next().ok_or_else(err)?;
157 let blob_str = iter.next().ok_or_else(err)?;
158
159 let path = path_str.parse().map_err(|_| err())?;
160 let blob = blob_str.parse().map_err(|_| err())?;
161
162 OperationData::CreateFile(OperationCreateFile { path, blob })
163 }
164 OPERATION_KIND_CREATE_DIR => {
165 let path_str = iter.next().ok_or_else(err)?;
166
167 let path = path_str.parse().map_err(|_| err())?;
168
169 OperationData::CreateDir(OperationCreateDir { path })
170 }
171 OPERATION_KIND_RENAME => {
172 let old_str = iter.next().ok_or_else(err)?;
173 let new_str = iter.next().ok_or_else(err)?;
174
175 let old = old_str.parse().map_err(|_| err())?;
176 let new = new_str.parse().map_err(|_| err())?;
177
178 OperationData::Rename(OperationRename { old, new })
179 }
180 OPERATION_KIND_REMOVE => {
181 let path_str = iter.next().ok_or_else(err)?;
182
183 let path = path_str.parse().map_err(|_| err())?;
184
185 OperationData::Remove(OperationRemove { path })
186 }
187 _ => return Err(err()),
188 };
189
190 Ok(Self {
191 header: OperationHeader {
192 timestamp,
193 revision,
194 email,
195 },
196 data,
197 })
198 }
199}
200
201impl Display for Operation {
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 write!(
204 f,
205 "{}\t{}\t{}\t",
206 self.header.timestamp, self.header.revision, self.header.email
207 )?;
208 match &self.data {
209 OperationData::CreateFile(data) => write!(
210 f,
211 "{}\t{}\t{}",
212 OPERATION_KIND_CREATE_FILE, data.path, data.blob
213 ),
214 OperationData::CreateDir(data) => {
215 write!(f, "{}\t{}", OPERATION_KIND_CREATE_DIR, data.path)
216 }
217 OperationData::Remove(data) => write!(f, "{}\t{}", OPERATION_KIND_REMOVE, data.path),
218 OperationData::Rename(data) => {
219 write!(f, "{}\t{}\t{}", OPERATION_KIND_RENAME, data.old, data.new)
220 }
221 }
222 }
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Hash)]
226pub struct OperationHeader {
227 pub timestamp: u64,
228 pub revision: u64,
229 pub email: String,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Hash)]
233pub enum OperationData {
234 CreateFile(OperationCreateFile),
235 CreateDir(OperationCreateDir),
236 Remove(OperationRemove),
237 Rename(OperationRename),
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Hash)]
241pub struct OperationCreateFile {
242 pub path: DrivePath,
243 pub blob: BlobId,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Hash)]
247pub struct OperationCreateDir {
248 pub path: DrivePath,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Hash)]
252pub struct OperationRemove {
253 pub path: DrivePath,
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Hash)]
257pub struct OperationRename {
258 pub old: DrivePath,
259 pub new: DrivePath,
260}
261
262#[derive(Debug)]
263pub struct InvalidDrivePath(String);
264
265impl std::fmt::Display for InvalidDrivePath {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 write!(f, "invalid path: {}", self.0)
268 }
269}
270
271impl std::error::Error for InvalidDrivePath {}
272
273#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
274pub struct DrivePath(Vec<DrivePathComponent>);
275
276impl Display for DrivePath {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 if self.is_root() {
279 f.write_str("/")?;
280 } else {
281 for comp in self.components() {
282 f.write_str("/")?;
283 comp.fmt(f)?;
284 }
285 }
286 Ok(())
287 }
288}
289
290impl DrivePath {
291 pub fn is_root(&self) -> bool {
292 self.0.is_empty()
293 }
294
295 pub fn push(&self, component: DrivePathComponent) -> DrivePath {
296 let mut components = self.0.clone();
297 components.push(component);
298 Self(components)
299 }
300
301 pub fn components(&self) -> &[DrivePathComponent] {
302 self.0.as_slice()
303 }
304
305 pub fn parent(&self) -> DrivePath {
306 if self.0.is_empty() {
307 Self(Default::default())
308 } else {
309 let slice = self.0.as_slice();
310 Self(slice[..slice.len() - 1].to_owned())
311 }
312 }
313
314 pub fn last(&self) -> Option<DrivePathComponent> {
315 self.0.last().cloned()
316 }
317
318 pub fn split(&self) -> Option<(DrivePath, DrivePathComponent)> {
319 if self.0.is_empty() {
320 None
321 } else {
322 Some((
323 Self(self.0[..self.0.len() - 1].to_owned()),
324 self.0[self.0.len() - 1].clone(),
325 ))
326 }
327 }
328}
329
330impl FromStr for DrivePath {
331 type Err = InvalidDrivePath;
332
333 fn from_str(s: &str) -> Result<Self, Self::Err> {
334 let mut components = Vec::default();
335 for unchecked_component in s.trim().trim_matches('/').split('/') {
336 match unchecked_component.parse() {
337 Ok(component) => components.push(component),
338 Err(err) => {
339 return Err(InvalidDrivePath(format!(
340 "path contained invalid component '{unchecked_component}': {err}"
341 )));
342 }
343 };
344 }
345 Ok(Self(components))
346 }
347}
348
349#[derive(Debug)]
350pub struct InvalidDrivePathComponent(&'static str);
351
352impl Display for InvalidDrivePathComponent {
353 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
354 self.0.fmt(f)
355 }
356}
357
358impl InvalidDrivePathComponent {
359 pub const fn new(msg: &'static str) -> Self {
360 Self(msg)
361 }
362}
363
364#[derive(Debug, Clone, PartialEq, Eq, Hash)]
365pub struct DrivePathComponent(String);
366
367impl Display for DrivePathComponent {
368 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369 self.0.fmt(f)
370 }
371}
372
373impl DrivePathComponent {
374 pub fn as_str(&self) -> &str {
375 &self.0
376 }
377}
378
379impl FromStr for DrivePathComponent {
380 type Err = InvalidDrivePathComponent;
381
382 fn from_str(s: &str) -> Result<Self, Self::Err> {
383 if s.is_empty() {
384 return Err(InvalidDrivePathComponent::new(
385 "path component cannot be empty",
386 ));
387 }
388
389 if s.contains('\t') {
390 return Err(InvalidDrivePathComponent::new(
391 "path component cannot contain tabs",
392 ));
393 }
394
395 if s == "." || s == ".." {
396 return Err(InvalidDrivePathComponent::new(
397 "path component cannot be '.' or '..'",
398 ));
399 }
400
401 if s.contains('/') {
402 return Err(InvalidDrivePathComponent::new(
403 "path component cannot contain '/'",
404 ));
405 }
406
407 if s.len() > 256 {
408 return Err(InvalidDrivePathComponent::new("path component is too long"));
409 }
410
411 Ok(Self(s.to_string()))
412 }
413}
414
415#[derive(Debug)]
416pub struct FsError(String);
417
418impl From<String> for FsError {
419 fn from(value: String) -> Self {
420 Self(value)
421 }
422}
423
424impl From<&str> for FsError {
425 fn from(value: &str) -> Self {
426 Self(value.to_string())
427 }
428}
429
430#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
431pub enum FsNodeKind {
432 File,
433 Directory,
434}
435
436pub struct FsNode {
437 pub kind: FsNodeKind,
438 pub author: String,
439 pub lastmod: u64,
440 pub children: HashMap<DrivePathComponent, FsNodeId>,
441 pub blob: BlobId,
442}
443
444slotmap::new_key_type! { pub struct FsNodeId; }
445
446pub struct Fs {
447 pub root: FsNodeId,
448 pub nodes: SlotMap<FsNodeId, FsNode>,
449}
450
451impl Default for Fs {
452 fn default() -> Self {
453 let mut nodes: SlotMap<FsNodeId, FsNode> = Default::default();
454 let root = nodes.insert(FsNode {
455 kind: FsNodeKind::Directory,
456 author: "system".to_string(),
457 lastmod: 0,
458 children: Default::default(),
459 blob: Default::default(),
460 });
461 Self { root, nodes }
462 }
463}
464
465pub fn apply(fs: &mut Fs, op: &Operation) -> Result<(), FsError> {
466 match &op.data {
467 OperationData::CreateFile(data) => apply_create_file(fs, &op.header, &data),
468 OperationData::CreateDir(data) => apply_create_dir(fs, &op.header, &data),
469 OperationData::Remove(data) => apply_remove(fs, &op.header, &data),
470 OperationData::Rename(data) => apply_rename(fs, &op.header, &data),
471 }
472}
473
474pub fn apply_create_file(
475 fs: &mut Fs,
476 header: &OperationHeader,
477 data: &OperationCreateFile,
478) -> Result<(), FsError> {
479 if data.path.is_root() {
480 return Err(FsError::from("cannot create file at root"));
481 }
482
483 let mut parent_id = fs.root;
484 for comp in data.path.parent().components() {
485 let node = &mut fs.nodes[parent_id];
486 if node.kind != FsNodeKind::Directory {
487 return Err(FsError::from("cannot create file under another file"));
488 }
489 parent_id = match node.children.get(comp) {
490 Some(id) => *id,
491 None => {
492 let id = fs.nodes.insert(FsNode {
493 kind: FsNodeKind::Directory,
494 author: header.email.clone(),
495 lastmod: header.timestamp,
496 children: Default::default(),
497 blob: Default::default(),
498 });
499 fs.nodes[parent_id].children.insert(comp.clone(), id);
500 id
501 }
502 };
503 }
504
505 let name = data.path.last().unwrap();
506 let parent = &mut fs.nodes[parent_id];
507
508 if parent.kind != FsNodeKind::Directory {
509 return Err(FsError::from("cannot create file under another file"));
510 }
511
512 match parent.children.get(&name).copied() {
513 Some(node_id) => {
514 let node = &mut fs.nodes[node_id];
515 if node.kind == FsNodeKind::Directory {
516 return Err(FsError::from(
517 "node at path already exists but is a directory",
518 ));
519 }
520 node.author = header.email.clone();
521 node.lastmod = header.timestamp;
522 node.blob = data.blob.clone();
523 }
524 None => {
525 let id = fs.nodes.insert(FsNode {
526 kind: FsNodeKind::File,
527 author: header.email.clone(),
528 lastmod: header.timestamp,
529 children: Default::default(),
530 blob: data.blob.clone(),
531 });
532 fs.nodes[parent_id].children.insert(name, id);
533 }
534 }
535 Ok(())
536}
537
538pub fn apply_create_dir(
539 fs: &mut Fs,
540 header: &OperationHeader,
541 data: &OperationCreateDir,
542) -> Result<(), FsError> {
543 let (parent, name) = data
544 .path
545 .split()
546 .ok_or_else(|| FsError::from("cannot recreate root directory"))?;
547
548 let mut parent_id = fs.root;
549 for comp in parent.components() {
550 let node = &fs.nodes[parent_id];
551 parent_id = match node.children.get(comp) {
552 Some(&child_id) => {
553 let child = &fs.nodes[child_id];
554 if child.kind == FsNodeKind::File {
555 return Err(FsError::from("cannot create directory under file"));
556 }
557 child_id
558 }
559 None => {
560 let id = fs.nodes.insert(FsNode {
561 kind: FsNodeKind::Directory,
562 author: header.email.clone(),
563 lastmod: header.timestamp,
564 children: Default::default(),
565 blob: Default::default(),
566 });
567 fs.nodes[parent_id].children.insert(comp.clone(), id);
568 id
569 }
570 };
571 }
572
573 let parent = &fs.nodes[parent_id];
574 match parent.children.get(&name).copied() {
575 Some(child_id) => {
576 let child = &fs.nodes[child_id];
577 if child.kind != FsNodeKind::Directory {
578 return Err(FsError::from(
579 "cannot create directory, the given path is already a file",
580 ));
581 }
582 }
583 None => {
584 let id = fs.nodes.insert(FsNode {
585 kind: FsNodeKind::Directory,
586 author: header.email.clone(),
587 lastmod: header.timestamp,
588 children: Default::default(),
589 blob: Default::default(),
590 });
591
592 let parent = &mut fs.nodes[parent_id];
593 parent.children.insert(name, id);
594 }
595 }
596
597 Ok(())
598}
599
600pub fn apply_remove(
601 fs: &mut Fs,
602 _header: &OperationHeader,
603 data: &OperationRemove,
604) -> Result<(), FsError> {
605 let (parent, name) = data
606 .path
607 .split()
608 .ok_or_else(|| FsError::from("cannot remove root directory"))?;
609
610 let parent_id = match find_node(fs, &parent) {
611 Some(id) => id,
612 None => return Ok(()),
613 };
614
615 let parent = &mut fs.nodes[parent_id];
616 parent.children.remove(&name);
617
618 Ok(())
619}
620
621fn find_node(fs: &Fs, path: &DrivePath) -> Option<FsNodeId> {
622 let mut node_id = fs.root;
623 for comp in path.components() {
624 let node = &fs.nodes[node_id];
625 node_id = *node.children.get(comp)?;
626 }
627 Some(node_id)
628}
629
630pub fn apply_rename(
631 fs: &mut Fs,
632 _header: &OperationHeader,
633 data: &OperationRename,
634) -> Result<(), FsError> {
635 let (old_parent, old_name) = data
636 .old
637 .split()
638 .ok_or_else(|| FsError::from("cannot move root directory"))?;
639
640 let (new_parent, new_name) = data
641 .new
642 .split()
643 .ok_or_else(|| FsError::from("move destination cannot be root directory"))?;
644
645 let old_parent_id = find_node(fs, &old_parent).unwrap();
646 let old_parent = &mut fs.nodes[old_parent_id];
647 let node_id = old_parent.children.remove(&old_name).unwrap();
648
649 let new_parent_id = find_node(fs, &new_parent).unwrap();
650 let new_parent = &mut fs.nodes[new_parent_id];
651 new_parent.children.insert(new_name, node_id);
652
653 Ok(())
654}
655
656#[derive(Debug, Parser)]
657struct Cli {
658 #[clap(subcommand)]
659 cmd: Cmd,
660}
661
662#[derive(Debug, Subcommand)]
663enum Cmd {
664 CreateFile(CreateFileArgs),
665 CreateDir(CreateDirArgs),
666 Remove(RemoveArgs),
667 Rename(RenameArgs),
668 View(ViewArgs),
669 Import(ImportArgs),
670}
671
672#[derive(Debug, Args)]
673struct CreateFileArgs {
674 #[clap(long)]
675 timestamp: Option<u64>,
676
677 #[clap(long)]
678 email: String,
679
680 #[clap(long)]
681 path: DrivePath,
682
683 #[clap(long)]
684 file: std::path::PathBuf,
685}
686
687#[derive(Debug, Args)]
688struct CreateDirArgs {
689 #[clap(long)]
690 timestamp: Option<u64>,
691
692 #[clap(long)]
693 email: String,
694
695 #[clap(long)]
696 path: DrivePath,
697}
698
699#[derive(Debug, Args)]
700struct RemoveArgs {
701 #[clap(long)]
702 timestamp: Option<u64>,
703
704 #[clap(long)]
705 email: String,
706
707 #[clap(long)]
708 path: DrivePath,
709}
710
711#[derive(Debug, Args)]
712struct RenameArgs {
713 #[clap(long)]
714 timestamp: Option<u64>,
715
716 #[clap(long)]
717 email: String,
718
719 #[clap(long)]
720 old: DrivePath,
721
722 #[clap(long)]
723 new: DrivePath,
724}
725
726#[derive(Debug, Args)]
727struct ViewArgs {}
728
729#[derive(Debug, Args)]
730struct ImportArgs {
731 #[clap(long)]
732 timestamp: Option<u64>,
733
734 #[clap(long)]
735 email: String,
736
737 path: PathBuf,
738}
739
740fn main() {
741 let cli = Cli::parse();
742
743 match cli.cmd {
744 Cmd::CreateFile(args) => cmd_create_file(args),
745 Cmd::CreateDir(args) => cmd_create_dir(args),
746 Cmd::Remove(args) => cmd_remove(args),
747 Cmd::Rename(args) => cmd_rename(args),
748 Cmd::View(args) => cmd_view(args),
749 Cmd::Import(args) => cmd_import(args),
750 }
751}
752
753fn cmd_create_file(args: CreateFileArgs) {
754 let file_blob_id = blob_hash_file(&args.file).unwrap();
755 let _lock = write_lock();
756
757 let mut fs = Fs::default();
758 let mut ops = read_log_file();
759 let store = BlobStore::new("blobs");
760 ops.iter().for_each(|op| apply(&mut fs, op).unwrap());
761
762 let timestamp = args.timestamp.unwrap_or_else(get_timestamp);
763 let revision = get_next_revision(&ops);
764 blob_import_file(&store, ImportMode::HardLink, &file_blob_id, &args.file).unwrap();
765
766 let new_op = Operation {
767 header: OperationHeader {
768 timestamp,
769 revision,
770 email: args.email,
771 },
772 data: OperationData::CreateFile(OperationCreateFile {
773 path: args.path,
774 blob: file_blob_id,
775 }),
776 };
777 apply(&mut fs, &new_op).unwrap();
778 ops.push(new_op);
779 write_log_file(&ops);
780}
781
782fn cmd_create_dir(args: CreateDirArgs) {
783 let _lock = write_lock();
784 let mut fs = Fs::default();
785 let mut ops = read_log_file();
786 ops.iter().for_each(|op| apply(&mut fs, op).unwrap());
787
788 let timestamp = args.timestamp.unwrap_or_else(get_timestamp);
789 let revision = get_next_revision(&ops);
790
791 let new_op = Operation {
792 header: OperationHeader {
793 timestamp,
794 revision,
795 email: args.email,
796 },
797 data: OperationData::CreateDir(OperationCreateDir { path: args.path }),
798 };
799 apply(&mut fs, &new_op).unwrap();
800 ops.push(new_op);
801 write_log_file(&ops);
802}
803
804fn cmd_remove(args: RemoveArgs) {
805 let _lock = write_lock();
806 let mut fs = Fs::default();
807 let mut ops = read_log_file();
808 ops.iter().for_each(|op| apply(&mut fs, op).unwrap());
809
810 let timestamp = args.timestamp.unwrap_or_else(get_timestamp);
811 let revision = get_next_revision(&ops);
812
813 let new_op = Operation {
814 header: OperationHeader {
815 timestamp,
816 revision,
817 email: args.email,
818 },
819 data: OperationData::Remove(OperationRemove { path: args.path }),
820 };
821 apply(&mut fs, &new_op).unwrap();
822 ops.push(new_op);
823 write_log_file(&ops);
824}
825
826fn cmd_rename(args: RenameArgs) {
827 let _lock = write_lock();
828 let mut fs = Fs::default();
829 let mut ops = read_log_file();
830 ops.iter().for_each(|op| apply(&mut fs, op).unwrap());
831
832 let timestamp = args.timestamp.unwrap_or_else(get_timestamp);
833 let revision = get_next_revision(&ops);
834
835 let new_op = Operation {
836 header: OperationHeader {
837 timestamp,
838 revision,
839 email: args.email,
840 },
841 data: OperationData::Rename(OperationRename {
842 old: args.old,
843 new: args.new,
844 }),
845 };
846
847 apply(&mut fs, &new_op).unwrap();
848 ops.push(new_op);
849 write_log_file(&ops);
850}
851
852fn cmd_view(args: ViewArgs) {
853 let mut fs = Fs::default();
854 let ops = read_log_file();
855 ops.iter().for_each(|op| apply(&mut fs, op).unwrap());
856 write_node(&fs, fs.root, Default::default());
857}
858
859#[derive(Debug, Clone)]
860struct Queue<T>(Arc<Mutex<VecDeque<T>>>);
861
862impl<T> Default for Queue<T> {
863 fn default() -> Self {
864 Self(Arc::new(Mutex::new(Default::default())))
865 }
866}
867
868impl<T> From<Vec<T>> for Queue<T> {
869 fn from(value: Vec<T>) -> Self {
870 Self(Arc::new(Mutex::new(value.into())))
871 }
872}
873
874impl<T> Queue<T> {
875 pub fn push(&self, v: T) {
876 self.0.lock().unwrap().push_back(v);
877 }
878
879 pub fn pop(&self) -> Option<T> {
880 self.0.lock().unwrap().pop_front()
881 }
882}
883
884fn cmd_import(args: ImportArgs) {
885 let _lock = write_lock();
886
887 let mut ops = read_log_file();
888
889 let store = BlobStore::new("blobs");
890 let timestamp = args.timestamp.unwrap_or_else(get_timestamp);
891 let root = args.path.canonicalize().unwrap();
892
893 let files = Queue::from(collect_all_file_paths(&root));
894 let num_threads = std::thread::available_parallelism().unwrap().get();
895 let mut handles = Vec::default();
896 for _ in 0..num_threads {
897 let root = root.clone();
898 let email = args.email.clone();
899 let files = files.clone();
900 let store = store.clone();
901 let handle = std::thread::spawn(move || {
902 let mut ops = Vec::default();
903 while let Some(file) = files.pop() {
904 let rel = file.strip_prefix(&root).unwrap();
905 let drive_path = rel.to_str().unwrap().parse::<DrivePath>().unwrap();
906 let blob_id = blob_hash_file(&file).unwrap();
907 blob_import_file(&store, ImportMode::HardLink, &blob_id, &file).unwrap();
908 let op = Operation {
909 header: OperationHeader {
910 timestamp,
911 revision: 0,
912 email: email.clone(),
913 },
914 data: OperationData::CreateFile(OperationCreateFile {
915 path: drive_path,
916 blob: blob_id,
917 }),
918 };
919 ops.push(op);
920 }
921 ops
922 });
923 handles.push(handle);
924 }
925
926 let mut fs = Fs::default();
927 let mut next_rev = get_next_revision(&ops);
928 for handle in handles {
929 let mut task_ops = handle.join().unwrap();
930 for op in &mut task_ops {
931 op.header.revision = next_rev;
932 next_rev += 1;
933 }
934 ops.extend(task_ops);
935 }
936 ops.iter().for_each(|op| apply(&mut fs, op).unwrap());
937 write_log_file(&ops);
938}
939
940fn collect_all_file_paths(root: &Path) -> Vec<PathBuf> {
941 let mut queue = vec![root.to_path_buf()];
942 let mut files = vec![];
943 while let Some(path) = queue.pop() {
944 if path.is_dir() {
945 let mut read = path.read_dir().unwrap();
946 while let Some(entry) = read.next() {
947 let entry = entry.unwrap();
948 queue.push(entry.path());
949 }
950 } else {
951 files.push(path);
952 }
953 }
954 files
955}
956
957fn write_node(fs: &Fs, node_id: FsNodeId, path: DrivePath) {
958 let node = &fs.nodes[node_id];
959 match node.kind {
960 FsNodeKind::File => println!(
961 "{}\tfile\t{}\t{}\t{}",
962 path, node.lastmod, node.blob, node.author
963 ),
964 FsNodeKind::Directory => {
965 println!("{}\tdir\t{}\t-\t{}", path, node.lastmod, node.author);
966 for (child_comp, child_id) in node.children.iter() {
967 let child_path = path.push(child_comp.clone());
968 write_node(fs, *child_id, child_path);
969 }
970 }
971 }
972}
973
974fn read_log_file() -> Vec<Operation> {
975 let mut operations = Vec::default();
976 if std::fs::exists("log.txt").unwrap() {
977 let contents = std::fs::read_to_string("log.txt").unwrap();
978 for line in contents.lines() {
979 let operation = line.parse().unwrap();
980 operations.push(operation);
981 }
982 }
983 operations
984}
985
986fn write_log_file(ops: &[Operation]) {
987 {
988 let file = File::options()
989 .create(true)
990 .write(true)
991 .truncate(true)
992 .open("log.txt.tmp")
993 .unwrap();
994 let mut writer = BufWriter::new(file);
995 for op in ops {
996 writeln!(writer, "{op}").unwrap();
997 }
998 writer.flush().unwrap();
999 }
1000 std::fs::rename("log.txt.tmp", "log.txt").unwrap();
1001}
1002
1003fn get_timestamp() -> u64 {
1004 SystemTime::now()
1005 .duration_since(SystemTime::UNIX_EPOCH)
1006 .unwrap()
1007 .as_secs()
1008}
1009
1010fn get_next_revision(ops: &[Operation]) -> u64 {
1011 match ops.last() {
1012 Some(op) => op.header.revision + 1,
1013 None => 0,
1014 }
1015}
1016
1017fn write_lock() -> File {
1018 let file = std::fs::OpenOptions::new()
1019 .write(true)
1020 .create(true)
1021 .open("write.lock")
1022 .unwrap();
1023 file.lock().unwrap();
1024 file
1025}