summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore6
-rw-r--r--NOTES.md3
-rwxr-xr-xauth.sh8
-rw-r--r--deployment.yaml28
-rw-r--r--fctdrive/Cargo.lock351
-rw-r--r--fctdrive/Cargo.toml13
-rw-r--r--fctdrive/src/main.rs1025
-rw-r--r--filesystem.go244
-rw-r--r--go.mod8
-rw-r--r--go.sum4
-rw-r--r--index.html17
-rw-r--r--main.go75
-rw-r--r--path.go75
-rw-r--r--service.yaml0
14 files changed, 1398 insertions, 459 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..99ffd03
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
1/miei/
2/drive/
3/blobs/
4/fctdrive/blobs/
5write.lock
6log.txt
diff --git a/NOTES.md b/NOTES.md
new file mode 100644
index 0000000..8d55dcb
--- /dev/null
+++ b/NOTES.md
@@ -0,0 +1,3 @@
1# fct drive
2
3
diff --git a/auth.sh b/auth.sh
deleted file mode 100755
index aa3dc1f..0000000
--- a/auth.sh
+++ /dev/null
@@ -1,8 +0,0 @@
1#!/usr/bin/env bash
2
3podman run -it --rm --network host \
4 -e SECRET="12345678912345678911111111111111" \
5 -e APP_URL="http://10.0.0.92:3000" \
6 -e USERS='diogo464:$2a$10$fE78J/Rq7kSik1cmXQ82Be6.zx3P4GEjhlifnI.ACHpHb5sDH/J1W' \
7 ghcr.io/steveiliop56/tinyauth:v3
8
diff --git a/deployment.yaml b/deployment.yaml
deleted file mode 100644
index 3bcd9cc..0000000
--- a/deployment.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
1---
2apiVersion: apps/v1
3kind: Deployment
4metadata:
5 name: auth
6spec:
7 selector:
8 matchLabels:
9 app: auth
10 template:
11 metadata:
12 labels:
13 app: auth
14 app.kubernetes.io/name: auth
15 spec:
16 containers:
17 - name: auth
18 image: ghcr.io/steveiliop56/tinyauth:v3
19 resources:
20 requests:
21 memory: "64Mi"
22 cpu: "0"
23 limits:
24 memory: "256Mi"
25 cpu: "1"
26 ports:
27 - name: http
28 containerPort: 8000
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}
diff --git a/filesystem.go b/filesystem.go
deleted file mode 100644
index 236f704..0000000
--- a/filesystem.go
+++ /dev/null
@@ -1,244 +0,0 @@
1package main
2
3import "fmt"
4
5type Filesystem_Ent_Type string
6
7const (
8 Filesystem_Ent_Type_Dir Filesystem_Ent_Type = "dir"
9 Filesystem_Ent_Type_File Filesystem_Ent_Type = "file"
10)
11
12type Filesystem struct {
13 Root *Filesystem_Ent
14}
15
16func NewFilesystem() *Filesystem {
17 return &Filesystem{
18 Root: &Filesystem_Ent{
19 Type: Filesystem_Ent_Type_Dir,
20 Children: make(map[PathComponent]*Filesystem_Ent),
21 },
22 }
23}
24
25type Filesystem_Ent struct {
26 Type Filesystem_Ent_Type
27 Children map[PathComponent]*Filesystem_Ent
28 Blob string
29}
30
31func (fs *Filesystem) GetEntry(path Path) *Filesystem_Ent {
32 current := fs.Root
33 for _, component := range path.Components() {
34 if current == nil || current.Type != Filesystem_Ent_Type_Dir {
35 return nil
36 }
37 current = current.Children[component]
38 }
39 return current
40}
41
42func (fs *Filesystem) EnsureParentDirs(path Path) error {
43 if path.IsRoot() {
44 return nil
45 }
46
47 parentPath := path.Parent()
48 current := fs.Root
49
50 for _, component := range parentPath.Components() {
51 if current.Children == nil {
52 current.Children = make(map[PathComponent]*Filesystem_Ent)
53 }
54
55 if next, exists := current.Children[component]; exists {
56 if next.Type != Filesystem_Ent_Type_Dir {
57 return fmt.Errorf("path component '%s' is a file, cannot create directory", component)
58 }
59 current = next
60 } else {
61 newDir := &Filesystem_Ent{
62 Type: Filesystem_Ent_Type_Dir,
63 Children: make(map[PathComponent]*Filesystem_Ent),
64 }
65 current.Children[component] = newDir
66 current = newDir
67 }
68 }
69
70 return nil
71}
72
73func (fs *Filesystem) ApplyOperation(op *Operation) error {
74 switch op.Type {
75 case Operation_Type_CreateFile:
76 return fs.ApplyOperation_CreateFile(op)
77 case Operation_Type_CreateDir:
78 return fs.ApplyOperation_CreateDir(op)
79 case Operation_Type_Remove:
80 return fs.ApplyOperation_Remove(op)
81 case Operation_Type_Rename:
82 return fs.ApplyOperation_Rename(op)
83 default:
84 return fmt.Errorf("unhandled operation type: %s", op.Type)
85 }
86}
87
88func (fs *Filesystem) ApplyOperation_CreateFile(op *Operation) error {
89 params := op.GetCreateFileParams()
90
91 path, err := ParsePath(params.Path)
92 if err != nil {
93 return fmt.Errorf("invalid path '%s': %w", params.Path, err)
94 }
95
96 if path.IsRoot() {
97 return fmt.Errorf("cannot create file at root path")
98 }
99
100 if err := fs.EnsureParentDirs(path); err != nil {
101 return fmt.Errorf("failed to create parent directories: %w", err)
102 }
103
104 parentPath := path.Parent()
105 parent := fs.GetEntry(parentPath)
106 if parent == nil || parent.Type != Filesystem_Ent_Type_Dir {
107 return fmt.Errorf("parent is not a directory")
108 }
109
110 if parent.Children == nil {
111 parent.Children = make(map[PathComponent]*Filesystem_Ent)
112 }
113
114 filename := path.Components()[len(path.Components())-1]
115 parent.Children[filename] = &Filesystem_Ent{
116 Type: Filesystem_Ent_Type_File,
117 Blob: params.Blob,
118 }
119
120 return nil
121}
122
123func (fs *Filesystem) ApplyOperation_CreateDir(op *Operation) error {
124 params := op.GetCreateDirParams()
125
126 path, err := ParsePath(params.Path)
127 if err != nil {
128 return fmt.Errorf("invalid path '%s': %w", params.Path, err)
129 }
130
131 if path.IsRoot() {
132 return nil
133 }
134
135 existing := fs.GetEntry(path)
136 if existing != nil {
137 if existing.Type == Filesystem_Ent_Type_Dir {
138 return nil
139 }
140 return fmt.Errorf("cannot create directory '%s': file already exists", params.Path)
141 }
142
143 if err := fs.EnsureParentDirs(path); err != nil {
144 return fmt.Errorf("failed to create parent directories: %w", err)
145 }
146
147 parentPath := path.Parent()
148 parent := fs.GetEntry(parentPath)
149 if parent == nil || parent.Type != Filesystem_Ent_Type_Dir {
150 return fmt.Errorf("parent is not a directory")
151 }
152
153 if parent.Children == nil {
154 parent.Children = make(map[PathComponent]*Filesystem_Ent)
155 }
156
157 dirname := path.Components()[len(path.Components())-1]
158 parent.Children[dirname] = &Filesystem_Ent{
159 Type: Filesystem_Ent_Type_Dir,
160 Children: make(map[PathComponent]*Filesystem_Ent),
161 }
162
163 return nil
164}
165
166func (fs *Filesystem) ApplyOperation_Remove(op *Operation) error {
167 params := op.GetRemoveParams()
168
169 path, err := ParsePath(params.Path)
170 if err != nil {
171 return fmt.Errorf("invalid path '%s': %w", params.Path, err)
172 }
173
174 if path.IsRoot() {
175 return fmt.Errorf("cannot remove root directory")
176 }
177
178 existing := fs.GetEntry(path)
179 if existing == nil {
180 return nil
181 }
182
183 parentPath := path.Parent()
184 parent := fs.GetEntry(parentPath)
185 if parent == nil || parent.Type != Filesystem_Ent_Type_Dir {
186 return fmt.Errorf("parent is not a directory")
187 }
188
189 filename := path.Components()[len(path.Components())-1]
190 delete(parent.Children, filename)
191
192 return nil
193}
194
195func (fs *Filesystem) ApplyOperation_Rename(op *Operation) error {
196 params := op.GetRenameParams()
197
198 oldPath, err := ParsePath(params.Old)
199 if err != nil {
200 return fmt.Errorf("invalid old path '%s': %w", params.Old, err)
201 }
202
203 newPath, err := ParsePath(params.New)
204 if err != nil {
205 return fmt.Errorf("invalid new path '%s': %w", params.New, err)
206 }
207
208 if oldPath.IsRoot() || newPath.IsRoot() {
209 return fmt.Errorf("cannot rename root directory")
210 }
211
212 existing := fs.GetEntry(oldPath)
213 if existing == nil {
214 return nil
215 }
216
217 if err := fs.EnsureParentDirs(newPath); err != nil {
218 return fmt.Errorf("failed to create parent directories for new path: %w", err)
219 }
220
221 oldParentPath := oldPath.Parent()
222 oldParent := fs.GetEntry(oldParentPath)
223 if oldParent == nil || oldParent.Type != Filesystem_Ent_Type_Dir {
224 return fmt.Errorf("old parent is not a directory")
225 }
226
227 newParentPath := newPath.Parent()
228 newParent := fs.GetEntry(newParentPath)
229 if newParent == nil || newParent.Type != Filesystem_Ent_Type_Dir {
230 return fmt.Errorf("new parent is not a directory")
231 }
232
233 if newParent.Children == nil {
234 newParent.Children = make(map[PathComponent]*Filesystem_Ent)
235 }
236
237 oldFilename := oldPath.Components()[len(oldPath.Components())-1]
238 newFilename := newPath.Components()[len(newPath.Components())-1]
239
240 newParent.Children[newFilename] = existing
241 delete(oldParent.Children, oldFilename)
242
243 return nil
244}
diff --git a/go.mod b/go.mod
deleted file mode 100644
index 54ccc54..0000000
--- a/go.mod
+++ /dev/null
@@ -1,8 +0,0 @@
1module git.d464.sh/fctdrive
2
3go 1.24.5
4
5require (
6 github.com/go-chi/chi/v5 v5.2.2
7 github.com/pkg/errors v0.9.1
8)
diff --git a/go.sum b/go.sum
deleted file mode 100644
index 4085610..0000000
--- a/go.sum
+++ /dev/null
@@ -1,4 +0,0 @@
1github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
2github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
3github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
4github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
diff --git a/index.html b/index.html
deleted file mode 100644
index 169ebc0..0000000
--- a/index.html
+++ /dev/null
@@ -1,17 +0,0 @@
1<!DOCTYPE html>
2<html lang="en">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport" content="width=device-width, initial-scale=1">
7 <title>FCT Drive</title>
8</head>
9
10<body>
11 <form enctype="multipart/form-data" method="post" action="/upload">
12 <input type="file" name="file">
13 <input type="submit" name="submit" value="Upload">
14 </form>
15</body>
16
17</html>
diff --git a/main.go b/main.go
deleted file mode 100644
index 1be1067..0000000
--- a/main.go
+++ /dev/null
@@ -1,75 +0,0 @@
1package main
2
3import (
4 "encoding/json"
5 "net/http"
6 "os"
7
8 "github.com/go-chi/chi/v5"
9)
10
11type App struct {
12 Operations []*Operation
13 Filesystem *Filesystem
14}
15
16func main() {
17 router := chi.NewRouter()
18
19 app := &App{
20 Operations: []*Operation{},
21 Filesystem: NewFilesystem(),
22 }
23
24 router.Get("/", func(w http.ResponseWriter, r *http.Request) {
25 w.Header().Add("Content-Type", "text/html")
26 file, _ := os.ReadFile("index.html")
27 w.Write(file)
28 })
29 router.Route("/api", func(r chi.Router) {
30 r.Get("/view", view)
31 r.Get("/filesystem/*", h(app, filesystemGet))
32 r.Post("/filesystem/*", h(app, filesystemPost))
33 r.Delete("/filesystem/*", h(app, filesystemDelete))
34 })
35
36 if err := http.ListenAndServe("0.0.0.0:5000", router); err != nil {
37 panic(err)
38 }
39}
40
41func h(app *App, f func(*App, http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
42 return func(w http.ResponseWriter, r *http.Request) {
43 f(app, w, r)
44 }
45}
46
47func view(w http.ResponseWriter, r *http.Request) {
48 w.Write([]byte("Hi"))
49}
50
51func filesystemGet(app *App, w http.ResponseWriter, r *http.Request) {
52 path, err := ParsePath("/" + chi.URLParam(r, "*"))
53 if err != nil {
54 panic(err)
55 }
56
57 // response := map[string]interface{}{
58 // "path": path,
59 // "message": "Filesystem path requested",
60 // }
61
62 entry := app.Filesystem.GetEntry(path)
63
64 w.Header().Set("Content-Type", "application/json")
65 json.NewEncoder(w).Encode(entry)
66}
67
68func filesystemPost(app *App, w http.ResponseWriter, r *http.Request) {
69 if err := r.ParseMultipartForm(1024 * 1024); err != nil {
70 panic(err)
71 }
72
73}
74
75func filesystemDelete(app *App, w http.ResponseWriter, r *http.Request) {}
diff --git a/path.go b/path.go
deleted file mode 100644
index bb81229..0000000
--- a/path.go
+++ /dev/null
@@ -1,75 +0,0 @@
1package main
2
3import (
4 "fmt"
5 "strings"
6)
7
8type PathComponent string
9
10type Path []PathComponent
11
12func NewPathComponent(s string) (PathComponent, error) {
13 if s != strings.TrimSpace(s) {
14 return PathComponent(""), fmt.Errorf("path component cannot contain leading or trailing spaces")
15 }
16 if strings.Contains(s, "/") {
17 return PathComponent(""), fmt.Errorf("path component cannot contain '/'")
18 }
19 if len(s) == 0 {
20 return PathComponent(""), fmt.Errorf("path component cannot be empty")
21 }
22 return PathComponent(s), nil
23}
24
25func (c PathComponent) String() string {
26 return string(c)
27}
28
29func (p Path) Components() []PathComponent {
30 return []PathComponent(p)
31}
32
33func (p Path) Parent() Path {
34 components := []PathComponent(p)
35 if len(components) == 0 {
36 return Path(components)
37 } else {
38 return Path(components[:len(components)-1])
39 }
40}
41
42func (p Path) IsRoot() bool {
43 return len(p.Components()) == 0
44}
45
46func ParsePath(pathStr string) (Path, error) {
47 if pathStr == "" || pathStr == "/" {
48 return Path{}, nil
49 }
50
51 if !strings.HasPrefix(pathStr, "/") {
52 return nil, fmt.Errorf("path must be absolute, got: %s", pathStr)
53 }
54
55 pathStr = strings.TrimPrefix(pathStr, "/")
56 if len(pathStr) == 0 {
57 return Path{}, nil
58 }
59
60 parts := strings.Split(pathStr, "/")
61 components := make([]PathComponent, 0, len(parts))
62
63 for _, part := range parts {
64 if len(part) == 0 {
65 return nil, fmt.Errorf("empty path component in: %s", pathStr)
66 }
67 component, err := NewPathComponent(part)
68 if err != nil {
69 return nil, fmt.Errorf("invalid path component '%s': %w", part, err)
70 }
71 components = append(components, component)
72 }
73
74 return Path(components), nil
75}
diff --git a/service.yaml b/service.yaml
deleted file mode 100644
index e69de29..0000000
--- a/service.yaml
+++ /dev/null