From bebf1cd5fc4668b7970e3e2e426ad103ecbc670c Mon Sep 17 00:00:00 2001 From: diogo464 Date: Fri, 8 Aug 2025 17:40:19 +0100 Subject: rust cli init --- .gitignore | 6 + NOTES.md | 3 + auth.sh | 8 - deployment.yaml | 28 -- fctdrive/Cargo.lock | 351 +++++++++++++++++ fctdrive/Cargo.toml | 13 + fctdrive/src/main.rs | 1025 ++++++++++++++++++++++++++++++++++++++++++++++++++ filesystem.go | 244 ------------ go.mod | 8 - go.sum | 4 - index.html | 17 - main.go | 75 ---- path.go | 75 ---- service.yaml | 0 14 files changed, 1398 insertions(+), 459 deletions(-) create mode 100644 .gitignore create mode 100644 NOTES.md delete mode 100755 auth.sh delete mode 100644 deployment.yaml create mode 100644 fctdrive/Cargo.lock create mode 100644 fctdrive/Cargo.toml create mode 100644 fctdrive/src/main.rs delete mode 100644 filesystem.go delete mode 100644 go.mod delete mode 100644 go.sum delete mode 100644 index.html delete mode 100644 main.go delete mode 100644 path.go delete mode 100644 service.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99ffd03 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/miei/ +/drive/ +/blobs/ +/fctdrive/blobs/ +write.lock +log.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 @@ +# fct drive + + diff --git a/auth.sh b/auth.sh deleted file mode 100755 index aa3dc1f..0000000 --- a/auth.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -podman run -it --rm --network host \ - -e SECRET="12345678912345678911111111111111" \ - -e APP_URL="http://10.0.0.92:3000" \ - -e USERS='diogo464:$2a$10$fE78J/Rq7kSik1cmXQ82Be6.zx3P4GEjhlifnI.ACHpHb5sDH/J1W' \ - ghcr.io/steveiliop56/tinyauth:v3 - diff --git a/deployment.yaml b/deployment.yaml deleted file mode 100644 index 3bcd9cc..0000000 --- a/deployment.yaml +++ /dev/null @@ -1,28 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: auth -spec: - selector: - matchLabels: - app: auth - template: - metadata: - labels: - app: auth - app.kubernetes.io/name: auth - spec: - containers: - - name: auth - image: ghcr.io/steveiliop56/tinyauth:v3 - resources: - requests: - memory: "64Mi" - cpu: "0" - limits: - memory: "256Mi" - cpu: "1" - ports: - - name: http - 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 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "clap" +version = "4.5.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "fctdrive" +version = "0.1.0" +dependencies = [ + "clap", + "hex", + "sha2", + "slotmap", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "1.1.0" +source = "git+https://git.d464.sh/hex-rs#ed091ffa8206658262a0e2409a35d8910e5d0682" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "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 @@ +[package] +name = "fctdrive" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.5.43", features = ["derive", "env"] } +hex = { git = "https://git.d464.sh/hex-rs", version = "1.1.0" } +sha2 = "0.10.9" +slotmap = "1.0.7" + +[profile.release] +debug = 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 @@ +use std::{ + collections::{HashMap, VecDeque}, + fmt::Display, + fs::File, + io::{BufWriter, Read as _, Write}, + path::{Path, PathBuf}, + str::FromStr, + sync::{Arc, Mutex}, + time::SystemTime, +}; + +use clap::{Args, Parser, Subcommand}; +use sha2::Digest; +use slotmap::SlotMap; + +const BLOB_STORE_HIERARCHY_DEPTH: usize = 4; + +#[derive(Debug)] +pub struct InvalidBlobId; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub struct BlobId([u8; 32]); + +impl BlobId { + pub fn hash(&self, data: &[u8]) -> BlobId { + let mut hasher = sha2::Sha256::default(); + hasher.write_all(data).unwrap(); + Self(hasher.finalize().try_into().unwrap()) + } + + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +impl Display for BlobId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + hex::display(&self.0).fmt(f) + } +} + +impl FromStr for BlobId { + type Err = InvalidBlobId; + + fn from_str(s: &str) -> Result { + let decoded = hex::decode_hex(s).map_err(|_| InvalidBlobId)?; + Ok(Self(decoded.try_into().map_err(|_| InvalidBlobId)?)) + } +} + +#[derive(Debug, Clone)] +pub struct BlobStore(PathBuf); + +impl BlobStore { + pub fn new(path: impl Into) -> Self { + Self(path.into()) + } +} + +pub fn blob_path(store: &BlobStore, blob_id: &BlobId) -> PathBuf { + let encoded = hex::encode_hex(blob_id.as_bytes()); + let mut path = store.0.clone(); + for depth in 0..BLOB_STORE_HIERARCHY_DEPTH { + let base_idx = depth * 2; + path.push(&encoded[base_idx..base_idx + 2]); + } + path.push(encoded); + path +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ImportMode { + Move, + Copy, + HardLink, +} + +pub fn blob_import_file( + store: &BlobStore, + mode: ImportMode, + blob_id: &BlobId, + file_path: &Path, +) -> std::io::Result<()> { + let blob_path = blob_path(store, blob_id); + if blob_path.exists() { + return Ok(()); + } + + if let Some(parent) = blob_path.parent() { + std::fs::create_dir_all(parent)?; + } + match mode { + ImportMode::Move => { + std::fs::rename(file_path, blob_path)?; + } + ImportMode::Copy => { + todo!() + } + ImportMode::HardLink => match std::fs::hard_link(file_path, blob_path) { + Ok(()) => {} + Err(err) => { + if err.kind() != std::io::ErrorKind::AlreadyExists { + panic!("{err}"); + } + } + }, + } + Ok(()) +} + +pub fn blob_hash_file(file_path: &Path) -> std::io::Result { + let mut file = std::fs::File::open(file_path)?; + let mut buf = vec![0u8; 1 * 1024 * 1024]; + let mut hasher = sha2::Sha256::default(); + loop { + let n = file.read(&mut buf)?; + if n == 0 { + break; + } + hasher.write_all(&buf[..n])?; + } + Ok(BlobId(hasher.finalize().try_into().unwrap())) +} + +const OPERATION_KIND_CREATE_FILE: &'static str = "create_file"; +const OPERATION_KIND_CREATE_DIR: &'static str = "create_dir"; +const OPERATION_KIND_REMOVE: &'static str = "remove"; +const OPERATION_KIND_RENAME: &'static str = "rename"; + +#[derive(Debug)] +pub struct InvalidOperation; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Operation { + pub header: OperationHeader, + pub data: OperationData, +} + +impl FromStr for Operation { + type Err = InvalidOperation; + + fn from_str(s: &str) -> Result { + let err = || InvalidOperation; + let mut iter = s.split('\t'); + + let timestamp_str = iter.next().ok_or_else(err)?; + let revision_str = iter.next().ok_or_else(err)?; + let email_str = iter.next().ok_or_else(err)?; + let kind_str = iter.next().ok_or_else(err)?; + + let timestamp = timestamp_str.parse().map_err(|_| err())?; + let revision = revision_str.parse().map_err(|_| err())?; + let email = email_str.to_string(); + let data = match kind_str { + OPERATION_KIND_CREATE_FILE => { + let path_str = iter.next().ok_or_else(err)?; + let blob_str = iter.next().ok_or_else(err)?; + + let path = path_str.parse().map_err(|_| err())?; + let blob = blob_str.parse().map_err(|_| err())?; + + OperationData::CreateFile(OperationCreateFile { path, blob }) + } + OPERATION_KIND_CREATE_DIR => { + let path_str = iter.next().ok_or_else(err)?; + + let path = path_str.parse().map_err(|_| err())?; + + OperationData::CreateDir(OperationCreateDir { path }) + } + OPERATION_KIND_RENAME => { + let old_str = iter.next().ok_or_else(err)?; + let new_str = iter.next().ok_or_else(err)?; + + let old = old_str.parse().map_err(|_| err())?; + let new = new_str.parse().map_err(|_| err())?; + + OperationData::Rename(OperationRename { old, new }) + } + OPERATION_KIND_REMOVE => { + let path_str = iter.next().ok_or_else(err)?; + + let path = path_str.parse().map_err(|_| err())?; + + OperationData::Remove(OperationRemove { path }) + } + _ => return Err(err()), + }; + + Ok(Self { + header: OperationHeader { + timestamp, + revision, + email, + }, + data, + }) + } +} + +impl Display for Operation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}\t{}\t{}\t", + self.header.timestamp, self.header.revision, self.header.email + )?; + match &self.data { + OperationData::CreateFile(data) => write!( + f, + "{}\t{}\t{}", + OPERATION_KIND_CREATE_FILE, data.path, data.blob + ), + OperationData::CreateDir(data) => { + write!(f, "{}\t{}", OPERATION_KIND_CREATE_DIR, data.path) + } + OperationData::Remove(data) => write!(f, "{}\t{}", OPERATION_KIND_REMOVE, data.path), + OperationData::Rename(data) => { + write!(f, "{}\t{}\t{}", OPERATION_KIND_RENAME, data.old, data.new) + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct OperationHeader { + pub timestamp: u64, + pub revision: u64, + pub email: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum OperationData { + CreateFile(OperationCreateFile), + CreateDir(OperationCreateDir), + Remove(OperationRemove), + Rename(OperationRename), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct OperationCreateFile { + pub path: DrivePath, + pub blob: BlobId, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct OperationCreateDir { + pub path: DrivePath, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct OperationRemove { + pub path: DrivePath, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct OperationRename { + pub old: DrivePath, + pub new: DrivePath, +} + +#[derive(Debug)] +pub struct InvalidDrivePath(String); + +impl std::fmt::Display for InvalidDrivePath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "invalid path: {}", self.0) + } +} + +impl std::error::Error for InvalidDrivePath {} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +pub struct DrivePath(Vec); + +impl Display for DrivePath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.is_root() { + f.write_str("/")?; + } else { + for comp in self.components() { + f.write_str("/")?; + comp.fmt(f)?; + } + } + Ok(()) + } +} + +impl DrivePath { + pub fn is_root(&self) -> bool { + self.0.is_empty() + } + + pub fn push(&self, component: DrivePathComponent) -> DrivePath { + let mut components = self.0.clone(); + components.push(component); + Self(components) + } + + pub fn components(&self) -> &[DrivePathComponent] { + self.0.as_slice() + } + + pub fn parent(&self) -> DrivePath { + if self.0.is_empty() { + Self(Default::default()) + } else { + let slice = self.0.as_slice(); + Self(slice[..slice.len() - 1].to_owned()) + } + } + + pub fn last(&self) -> Option { + self.0.last().cloned() + } + + pub fn split(&self) -> Option<(DrivePath, DrivePathComponent)> { + if self.0.is_empty() { + None + } else { + Some(( + Self(self.0[..self.0.len() - 1].to_owned()), + self.0[self.0.len() - 1].clone(), + )) + } + } +} + +impl FromStr for DrivePath { + type Err = InvalidDrivePath; + + fn from_str(s: &str) -> Result { + let mut components = Vec::default(); + for unchecked_component in s.trim().trim_matches('/').split('/') { + match unchecked_component.parse() { + Ok(component) => components.push(component), + Err(err) => { + return Err(InvalidDrivePath(format!( + "path contained invalid component '{unchecked_component}': {err}" + ))); + } + }; + } + Ok(Self(components)) + } +} + +#[derive(Debug)] +pub struct InvalidDrivePathComponent(&'static str); + +impl Display for InvalidDrivePathComponent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl InvalidDrivePathComponent { + pub const fn new(msg: &'static str) -> Self { + Self(msg) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DrivePathComponent(String); + +impl Display for DrivePathComponent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl DrivePathComponent { + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl FromStr for DrivePathComponent { + type Err = InvalidDrivePathComponent; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + return Err(InvalidDrivePathComponent::new( + "path component cannot be empty", + )); + } + + if s.contains('\t') { + return Err(InvalidDrivePathComponent::new( + "path component cannot contain tabs", + )); + } + + if s == "." || s == ".." { + return Err(InvalidDrivePathComponent::new( + "path component cannot be '.' or '..'", + )); + } + + if s.contains('/') { + return Err(InvalidDrivePathComponent::new( + "path component cannot contain '/'", + )); + } + + if s.len() > 256 { + return Err(InvalidDrivePathComponent::new("path component is too long")); + } + + Ok(Self(s.to_string())) + } +} + +#[derive(Debug)] +pub struct FsError(String); + +impl From for FsError { + fn from(value: String) -> Self { + Self(value) + } +} + +impl From<&str> for FsError { + fn from(value: &str) -> Self { + Self(value.to_string()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum FsNodeKind { + File, + Directory, +} + +pub struct FsNode { + pub kind: FsNodeKind, + pub author: String, + pub lastmod: u64, + pub children: HashMap, + pub blob: BlobId, +} + +slotmap::new_key_type! { pub struct FsNodeId; } + +pub struct Fs { + pub root: FsNodeId, + pub nodes: SlotMap, +} + +impl Default for Fs { + fn default() -> Self { + let mut nodes: SlotMap = Default::default(); + let root = nodes.insert(FsNode { + kind: FsNodeKind::Directory, + author: "system".to_string(), + lastmod: 0, + children: Default::default(), + blob: Default::default(), + }); + Self { root, nodes } + } +} + +pub fn apply(fs: &mut Fs, op: &Operation) -> Result<(), FsError> { + match &op.data { + OperationData::CreateFile(data) => apply_create_file(fs, &op.header, &data), + OperationData::CreateDir(data) => apply_create_dir(fs, &op.header, &data), + OperationData::Remove(data) => apply_remove(fs, &op.header, &data), + OperationData::Rename(data) => apply_rename(fs, &op.header, &data), + } +} + +pub fn apply_create_file( + fs: &mut Fs, + header: &OperationHeader, + data: &OperationCreateFile, +) -> Result<(), FsError> { + if data.path.is_root() { + return Err(FsError::from("cannot create file at root")); + } + + let mut parent_id = fs.root; + for comp in data.path.parent().components() { + let node = &mut fs.nodes[parent_id]; + if node.kind != FsNodeKind::Directory { + return Err(FsError::from("cannot create file under another file")); + } + parent_id = match node.children.get(comp) { + Some(id) => *id, + None => { + let id = fs.nodes.insert(FsNode { + kind: FsNodeKind::Directory, + author: header.email.clone(), + lastmod: header.timestamp, + children: Default::default(), + blob: Default::default(), + }); + fs.nodes[parent_id].children.insert(comp.clone(), id); + id + } + }; + } + + let name = data.path.last().unwrap(); + let parent = &mut fs.nodes[parent_id]; + + if parent.kind != FsNodeKind::Directory { + return Err(FsError::from("cannot create file under another file")); + } + + match parent.children.get(&name).copied() { + Some(node_id) => { + let node = &mut fs.nodes[node_id]; + if node.kind == FsNodeKind::Directory { + return Err(FsError::from( + "node at path already exists but is a directory", + )); + } + node.author = header.email.clone(); + node.lastmod = header.timestamp; + node.blob = data.blob.clone(); + } + None => { + let id = fs.nodes.insert(FsNode { + kind: FsNodeKind::File, + author: header.email.clone(), + lastmod: header.timestamp, + children: Default::default(), + blob: data.blob.clone(), + }); + fs.nodes[parent_id].children.insert(name, id); + } + } + Ok(()) +} + +pub fn apply_create_dir( + fs: &mut Fs, + header: &OperationHeader, + data: &OperationCreateDir, +) -> Result<(), FsError> { + let (parent, name) = data + .path + .split() + .ok_or_else(|| FsError::from("cannot recreate root directory"))?; + + let mut parent_id = fs.root; + for comp in parent.components() { + let node = &fs.nodes[parent_id]; + parent_id = match node.children.get(comp) { + Some(&child_id) => { + let child = &fs.nodes[child_id]; + if child.kind == FsNodeKind::File { + return Err(FsError::from("cannot create directory under file")); + } + child_id + } + None => { + let id = fs.nodes.insert(FsNode { + kind: FsNodeKind::Directory, + author: header.email.clone(), + lastmod: header.timestamp, + children: Default::default(), + blob: Default::default(), + }); + fs.nodes[parent_id].children.insert(comp.clone(), id); + id + } + }; + } + + let parent = &fs.nodes[parent_id]; + match parent.children.get(&name).copied() { + Some(child_id) => { + let child = &fs.nodes[child_id]; + if child.kind != FsNodeKind::Directory { + return Err(FsError::from( + "cannot create directory, the given path is already a file", + )); + } + } + None => { + let id = fs.nodes.insert(FsNode { + kind: FsNodeKind::Directory, + author: header.email.clone(), + lastmod: header.timestamp, + children: Default::default(), + blob: Default::default(), + }); + + let parent = &mut fs.nodes[parent_id]; + parent.children.insert(name, id); + } + } + + Ok(()) +} + +pub fn apply_remove( + fs: &mut Fs, + _header: &OperationHeader, + data: &OperationRemove, +) -> Result<(), FsError> { + let (parent, name) = data + .path + .split() + .ok_or_else(|| FsError::from("cannot remove root directory"))?; + + let parent_id = match find_node(fs, &parent) { + Some(id) => id, + None => return Ok(()), + }; + + let parent = &mut fs.nodes[parent_id]; + parent.children.remove(&name); + + Ok(()) +} + +fn find_node(fs: &Fs, path: &DrivePath) -> Option { + let mut node_id = fs.root; + for comp in path.components() { + let node = &fs.nodes[node_id]; + node_id = *node.children.get(comp)?; + } + Some(node_id) +} + +pub fn apply_rename( + fs: &mut Fs, + _header: &OperationHeader, + data: &OperationRename, +) -> Result<(), FsError> { + let (old_parent, old_name) = data + .old + .split() + .ok_or_else(|| FsError::from("cannot move root directory"))?; + + let (new_parent, new_name) = data + .new + .split() + .ok_or_else(|| FsError::from("move destination cannot be root directory"))?; + + let old_parent_id = find_node(fs, &old_parent).unwrap(); + let old_parent = &mut fs.nodes[old_parent_id]; + let node_id = old_parent.children.remove(&old_name).unwrap(); + + let new_parent_id = find_node(fs, &new_parent).unwrap(); + let new_parent = &mut fs.nodes[new_parent_id]; + new_parent.children.insert(new_name, node_id); + + Ok(()) +} + +#[derive(Debug, Parser)] +struct Cli { + #[clap(subcommand)] + cmd: Cmd, +} + +#[derive(Debug, Subcommand)] +enum Cmd { + CreateFile(CreateFileArgs), + CreateDir(CreateDirArgs), + Remove(RemoveArgs), + Rename(RenameArgs), + View(ViewArgs), + Import(ImportArgs), +} + +#[derive(Debug, Args)] +struct CreateFileArgs { + #[clap(long)] + timestamp: Option, + + #[clap(long)] + email: String, + + #[clap(long)] + path: DrivePath, + + #[clap(long)] + file: std::path::PathBuf, +} + +#[derive(Debug, Args)] +struct CreateDirArgs { + #[clap(long)] + timestamp: Option, + + #[clap(long)] + email: String, + + #[clap(long)] + path: DrivePath, +} + +#[derive(Debug, Args)] +struct RemoveArgs { + #[clap(long)] + timestamp: Option, + + #[clap(long)] + email: String, + + #[clap(long)] + path: DrivePath, +} + +#[derive(Debug, Args)] +struct RenameArgs { + #[clap(long)] + timestamp: Option, + + #[clap(long)] + email: String, + + #[clap(long)] + old: DrivePath, + + #[clap(long)] + new: DrivePath, +} + +#[derive(Debug, Args)] +struct ViewArgs {} + +#[derive(Debug, Args)] +struct ImportArgs { + #[clap(long)] + timestamp: Option, + + #[clap(long)] + email: String, + + path: PathBuf, +} + +fn main() { + let cli = Cli::parse(); + + match cli.cmd { + Cmd::CreateFile(args) => cmd_create_file(args), + Cmd::CreateDir(args) => cmd_create_dir(args), + Cmd::Remove(args) => cmd_remove(args), + Cmd::Rename(args) => cmd_rename(args), + Cmd::View(args) => cmd_view(args), + Cmd::Import(args) => cmd_import(args), + } +} + +fn cmd_create_file(args: CreateFileArgs) { + let file_blob_id = blob_hash_file(&args.file).unwrap(); + let _lock = write_lock(); + + let mut fs = Fs::default(); + let mut ops = read_log_file(); + let store = BlobStore::new("blobs"); + ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); + + let timestamp = args.timestamp.unwrap_or_else(get_timestamp); + let revision = get_next_revision(&ops); + blob_import_file(&store, ImportMode::HardLink, &file_blob_id, &args.file).unwrap(); + + let new_op = Operation { + header: OperationHeader { + timestamp, + revision, + email: args.email, + }, + data: OperationData::CreateFile(OperationCreateFile { + path: args.path, + blob: file_blob_id, + }), + }; + apply(&mut fs, &new_op).unwrap(); + ops.push(new_op); + write_log_file(&ops); +} + +fn cmd_create_dir(args: CreateDirArgs) { + let _lock = write_lock(); + let mut fs = Fs::default(); + let mut ops = read_log_file(); + ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); + + let timestamp = args.timestamp.unwrap_or_else(get_timestamp); + let revision = get_next_revision(&ops); + + let new_op = Operation { + header: OperationHeader { + timestamp, + revision, + email: args.email, + }, + data: OperationData::CreateDir(OperationCreateDir { path: args.path }), + }; + apply(&mut fs, &new_op).unwrap(); + ops.push(new_op); + write_log_file(&ops); +} + +fn cmd_remove(args: RemoveArgs) { + let _lock = write_lock(); + let mut fs = Fs::default(); + let mut ops = read_log_file(); + ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); + + let timestamp = args.timestamp.unwrap_or_else(get_timestamp); + let revision = get_next_revision(&ops); + + let new_op = Operation { + header: OperationHeader { + timestamp, + revision, + email: args.email, + }, + data: OperationData::Remove(OperationRemove { path: args.path }), + }; + apply(&mut fs, &new_op).unwrap(); + ops.push(new_op); + write_log_file(&ops); +} + +fn cmd_rename(args: RenameArgs) { + let _lock = write_lock(); + let mut fs = Fs::default(); + let mut ops = read_log_file(); + ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); + + let timestamp = args.timestamp.unwrap_or_else(get_timestamp); + let revision = get_next_revision(&ops); + + let new_op = Operation { + header: OperationHeader { + timestamp, + revision, + email: args.email, + }, + data: OperationData::Rename(OperationRename { + old: args.old, + new: args.new, + }), + }; + + apply(&mut fs, &new_op).unwrap(); + ops.push(new_op); + write_log_file(&ops); +} + +fn cmd_view(args: ViewArgs) { + let mut fs = Fs::default(); + let ops = read_log_file(); + ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); + write_node(&fs, fs.root, Default::default()); +} + +#[derive(Debug, Clone)] +struct Queue(Arc>>); + +impl Default for Queue { + fn default() -> Self { + Self(Arc::new(Mutex::new(Default::default()))) + } +} + +impl From> for Queue { + fn from(value: Vec) -> Self { + Self(Arc::new(Mutex::new(value.into()))) + } +} + +impl Queue { + pub fn push(&self, v: T) { + self.0.lock().unwrap().push_back(v); + } + + pub fn pop(&self) -> Option { + self.0.lock().unwrap().pop_front() + } +} + +fn cmd_import(args: ImportArgs) { + let _lock = write_lock(); + + let mut ops = read_log_file(); + + let store = BlobStore::new("blobs"); + let timestamp = args.timestamp.unwrap_or_else(get_timestamp); + let root = args.path.canonicalize().unwrap(); + + let files = Queue::from(collect_all_file_paths(&root)); + let num_threads = std::thread::available_parallelism().unwrap().get(); + let mut handles = Vec::default(); + for _ in 0..num_threads { + let root = root.clone(); + let email = args.email.clone(); + let files = files.clone(); + let store = store.clone(); + let handle = std::thread::spawn(move || { + let mut ops = Vec::default(); + while let Some(file) = files.pop() { + let rel = file.strip_prefix(&root).unwrap(); + let drive_path = rel.to_str().unwrap().parse::().unwrap(); + let blob_id = blob_hash_file(&file).unwrap(); + blob_import_file(&store, ImportMode::HardLink, &blob_id, &file).unwrap(); + let op = Operation { + header: OperationHeader { + timestamp, + revision: 0, + email: email.clone(), + }, + data: OperationData::CreateFile(OperationCreateFile { + path: drive_path, + blob: blob_id, + }), + }; + ops.push(op); + } + ops + }); + handles.push(handle); + } + + let mut fs = Fs::default(); + let mut next_rev = get_next_revision(&ops); + for handle in handles { + let mut task_ops = handle.join().unwrap(); + for op in &mut task_ops { + op.header.revision = next_rev; + next_rev += 1; + } + ops.extend(task_ops); + } + ops.iter().for_each(|op| apply(&mut fs, op).unwrap()); + write_log_file(&ops); +} + +fn collect_all_file_paths(root: &Path) -> Vec { + let mut queue = vec![root.to_path_buf()]; + let mut files = vec![]; + while let Some(path) = queue.pop() { + if path.is_dir() { + let mut read = path.read_dir().unwrap(); + while let Some(entry) = read.next() { + let entry = entry.unwrap(); + queue.push(entry.path()); + } + } else { + files.push(path); + } + } + files +} + +fn write_node(fs: &Fs, node_id: FsNodeId, path: DrivePath) { + let node = &fs.nodes[node_id]; + match node.kind { + FsNodeKind::File => println!( + "{}\tfile\t{}\t{}\t{}", + path, node.lastmod, node.blob, node.author + ), + FsNodeKind::Directory => { + println!("{}\tdir\t{}\t-\t{}", path, node.lastmod, node.author); + for (child_comp, child_id) in node.children.iter() { + let child_path = path.push(child_comp.clone()); + write_node(fs, *child_id, child_path); + } + } + } +} + +fn read_log_file() -> Vec { + let mut operations = Vec::default(); + if std::fs::exists("log.txt").unwrap() { + let contents = std::fs::read_to_string("log.txt").unwrap(); + for line in contents.lines() { + let operation = line.parse().unwrap(); + operations.push(operation); + } + } + operations +} + +fn write_log_file(ops: &[Operation]) { + { + let file = File::options() + .create(true) + .write(true) + .truncate(true) + .open("log.txt.tmp") + .unwrap(); + let mut writer = BufWriter::new(file); + for op in ops { + writeln!(writer, "{op}").unwrap(); + } + writer.flush().unwrap(); + } + std::fs::rename("log.txt.tmp", "log.txt").unwrap(); +} + +fn get_timestamp() -> u64 { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() +} + +fn get_next_revision(ops: &[Operation]) -> u64 { + match ops.last() { + Some(op) => op.header.revision + 1, + None => 0, + } +} + +fn write_lock() -> File { + let file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .open("write.lock") + .unwrap(); + file.lock().unwrap(); + file +} diff --git a/filesystem.go b/filesystem.go deleted file mode 100644 index 236f704..0000000 --- a/filesystem.go +++ /dev/null @@ -1,244 +0,0 @@ -package main - -import "fmt" - -type Filesystem_Ent_Type string - -const ( - Filesystem_Ent_Type_Dir Filesystem_Ent_Type = "dir" - Filesystem_Ent_Type_File Filesystem_Ent_Type = "file" -) - -type Filesystem struct { - Root *Filesystem_Ent -} - -func NewFilesystem() *Filesystem { - return &Filesystem{ - Root: &Filesystem_Ent{ - Type: Filesystem_Ent_Type_Dir, - Children: make(map[PathComponent]*Filesystem_Ent), - }, - } -} - -type Filesystem_Ent struct { - Type Filesystem_Ent_Type - Children map[PathComponent]*Filesystem_Ent - Blob string -} - -func (fs *Filesystem) GetEntry(path Path) *Filesystem_Ent { - current := fs.Root - for _, component := range path.Components() { - if current == nil || current.Type != Filesystem_Ent_Type_Dir { - return nil - } - current = current.Children[component] - } - return current -} - -func (fs *Filesystem) EnsureParentDirs(path Path) error { - if path.IsRoot() { - return nil - } - - parentPath := path.Parent() - current := fs.Root - - for _, component := range parentPath.Components() { - if current.Children == nil { - current.Children = make(map[PathComponent]*Filesystem_Ent) - } - - if next, exists := current.Children[component]; exists { - if next.Type != Filesystem_Ent_Type_Dir { - return fmt.Errorf("path component '%s' is a file, cannot create directory", component) - } - current = next - } else { - newDir := &Filesystem_Ent{ - Type: Filesystem_Ent_Type_Dir, - Children: make(map[PathComponent]*Filesystem_Ent), - } - current.Children[component] = newDir - current = newDir - } - } - - return nil -} - -func (fs *Filesystem) ApplyOperation(op *Operation) error { - switch op.Type { - case Operation_Type_CreateFile: - return fs.ApplyOperation_CreateFile(op) - case Operation_Type_CreateDir: - return fs.ApplyOperation_CreateDir(op) - case Operation_Type_Remove: - return fs.ApplyOperation_Remove(op) - case Operation_Type_Rename: - return fs.ApplyOperation_Rename(op) - default: - return fmt.Errorf("unhandled operation type: %s", op.Type) - } -} - -func (fs *Filesystem) ApplyOperation_CreateFile(op *Operation) error { - params := op.GetCreateFileParams() - - path, err := ParsePath(params.Path) - if err != nil { - return fmt.Errorf("invalid path '%s': %w", params.Path, err) - } - - if path.IsRoot() { - return fmt.Errorf("cannot create file at root path") - } - - if err := fs.EnsureParentDirs(path); err != nil { - return fmt.Errorf("failed to create parent directories: %w", err) - } - - parentPath := path.Parent() - parent := fs.GetEntry(parentPath) - if parent == nil || parent.Type != Filesystem_Ent_Type_Dir { - return fmt.Errorf("parent is not a directory") - } - - if parent.Children == nil { - parent.Children = make(map[PathComponent]*Filesystem_Ent) - } - - filename := path.Components()[len(path.Components())-1] - parent.Children[filename] = &Filesystem_Ent{ - Type: Filesystem_Ent_Type_File, - Blob: params.Blob, - } - - return nil -} - -func (fs *Filesystem) ApplyOperation_CreateDir(op *Operation) error { - params := op.GetCreateDirParams() - - path, err := ParsePath(params.Path) - if err != nil { - return fmt.Errorf("invalid path '%s': %w", params.Path, err) - } - - if path.IsRoot() { - return nil - } - - existing := fs.GetEntry(path) - if existing != nil { - if existing.Type == Filesystem_Ent_Type_Dir { - return nil - } - return fmt.Errorf("cannot create directory '%s': file already exists", params.Path) - } - - if err := fs.EnsureParentDirs(path); err != nil { - return fmt.Errorf("failed to create parent directories: %w", err) - } - - parentPath := path.Parent() - parent := fs.GetEntry(parentPath) - if parent == nil || parent.Type != Filesystem_Ent_Type_Dir { - return fmt.Errorf("parent is not a directory") - } - - if parent.Children == nil { - parent.Children = make(map[PathComponent]*Filesystem_Ent) - } - - dirname := path.Components()[len(path.Components())-1] - parent.Children[dirname] = &Filesystem_Ent{ - Type: Filesystem_Ent_Type_Dir, - Children: make(map[PathComponent]*Filesystem_Ent), - } - - return nil -} - -func (fs *Filesystem) ApplyOperation_Remove(op *Operation) error { - params := op.GetRemoveParams() - - path, err := ParsePath(params.Path) - if err != nil { - return fmt.Errorf("invalid path '%s': %w", params.Path, err) - } - - if path.IsRoot() { - return fmt.Errorf("cannot remove root directory") - } - - existing := fs.GetEntry(path) - if existing == nil { - return nil - } - - parentPath := path.Parent() - parent := fs.GetEntry(parentPath) - if parent == nil || parent.Type != Filesystem_Ent_Type_Dir { - return fmt.Errorf("parent is not a directory") - } - - filename := path.Components()[len(path.Components())-1] - delete(parent.Children, filename) - - return nil -} - -func (fs *Filesystem) ApplyOperation_Rename(op *Operation) error { - params := op.GetRenameParams() - - oldPath, err := ParsePath(params.Old) - if err != nil { - return fmt.Errorf("invalid old path '%s': %w", params.Old, err) - } - - newPath, err := ParsePath(params.New) - if err != nil { - return fmt.Errorf("invalid new path '%s': %w", params.New, err) - } - - if oldPath.IsRoot() || newPath.IsRoot() { - return fmt.Errorf("cannot rename root directory") - } - - existing := fs.GetEntry(oldPath) - if existing == nil { - return nil - } - - if err := fs.EnsureParentDirs(newPath); err != nil { - return fmt.Errorf("failed to create parent directories for new path: %w", err) - } - - oldParentPath := oldPath.Parent() - oldParent := fs.GetEntry(oldParentPath) - if oldParent == nil || oldParent.Type != Filesystem_Ent_Type_Dir { - return fmt.Errorf("old parent is not a directory") - } - - newParentPath := newPath.Parent() - newParent := fs.GetEntry(newParentPath) - if newParent == nil || newParent.Type != Filesystem_Ent_Type_Dir { - return fmt.Errorf("new parent is not a directory") - } - - if newParent.Children == nil { - newParent.Children = make(map[PathComponent]*Filesystem_Ent) - } - - oldFilename := oldPath.Components()[len(oldPath.Components())-1] - newFilename := newPath.Components()[len(newPath.Components())-1] - - newParent.Children[newFilename] = existing - delete(oldParent.Children, oldFilename) - - return nil -} diff --git a/go.mod b/go.mod deleted file mode 100644 index 54ccc54..0000000 --- a/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module git.d464.sh/fctdrive - -go 1.24.5 - -require ( - github.com/go-chi/chi/v5 v5.2.2 - github.com/pkg/errors v0.9.1 -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 4085610..0000000 --- a/go.sum +++ /dev/null @@ -1,4 +0,0 @@ -github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= -github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.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 @@ - - - - - - - FCT Drive - - - -
- - -
- - - diff --git a/main.go b/main.go deleted file mode 100644 index 1be1067..0000000 --- a/main.go +++ /dev/null @@ -1,75 +0,0 @@ -package main - -import ( - "encoding/json" - "net/http" - "os" - - "github.com/go-chi/chi/v5" -) - -type App struct { - Operations []*Operation - Filesystem *Filesystem -} - -func main() { - router := chi.NewRouter() - - app := &App{ - Operations: []*Operation{}, - Filesystem: NewFilesystem(), - } - - router.Get("/", func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "text/html") - file, _ := os.ReadFile("index.html") - w.Write(file) - }) - router.Route("/api", func(r chi.Router) { - r.Get("/view", view) - r.Get("/filesystem/*", h(app, filesystemGet)) - r.Post("/filesystem/*", h(app, filesystemPost)) - r.Delete("/filesystem/*", h(app, filesystemDelete)) - }) - - if err := http.ListenAndServe("0.0.0.0:5000", router); err != nil { - panic(err) - } -} - -func h(app *App, f func(*App, http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - f(app, w, r) - } -} - -func view(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("Hi")) -} - -func filesystemGet(app *App, w http.ResponseWriter, r *http.Request) { - path, err := ParsePath("/" + chi.URLParam(r, "*")) - if err != nil { - panic(err) - } - - // response := map[string]interface{}{ - // "path": path, - // "message": "Filesystem path requested", - // } - - entry := app.Filesystem.GetEntry(path) - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(entry) -} - -func filesystemPost(app *App, w http.ResponseWriter, r *http.Request) { - if err := r.ParseMultipartForm(1024 * 1024); err != nil { - panic(err) - } - -} - -func 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 @@ -package main - -import ( - "fmt" - "strings" -) - -type PathComponent string - -type Path []PathComponent - -func NewPathComponent(s string) (PathComponent, error) { - if s != strings.TrimSpace(s) { - return PathComponent(""), fmt.Errorf("path component cannot contain leading or trailing spaces") - } - if strings.Contains(s, "/") { - return PathComponent(""), fmt.Errorf("path component cannot contain '/'") - } - if len(s) == 0 { - return PathComponent(""), fmt.Errorf("path component cannot be empty") - } - return PathComponent(s), nil -} - -func (c PathComponent) String() string { - return string(c) -} - -func (p Path) Components() []PathComponent { - return []PathComponent(p) -} - -func (p Path) Parent() Path { - components := []PathComponent(p) - if len(components) == 0 { - return Path(components) - } else { - return Path(components[:len(components)-1]) - } -} - -func (p Path) IsRoot() bool { - return len(p.Components()) == 0 -} - -func ParsePath(pathStr string) (Path, error) { - if pathStr == "" || pathStr == "/" { - return Path{}, nil - } - - if !strings.HasPrefix(pathStr, "/") { - return nil, fmt.Errorf("path must be absolute, got: %s", pathStr) - } - - pathStr = strings.TrimPrefix(pathStr, "/") - if len(pathStr) == 0 { - return Path{}, nil - } - - parts := strings.Split(pathStr, "/") - components := make([]PathComponent, 0, len(parts)) - - for _, part := range parts { - if len(part) == 0 { - return nil, fmt.Errorf("empty path component in: %s", pathStr) - } - component, err := NewPathComponent(part) - if err != nil { - return nil, fmt.Errorf("invalid path component '%s': %w", part, err) - } - components = append(components, component) - } - - return Path(components), nil -} diff --git a/service.yaml b/service.yaml deleted file mode 100644 index e69de29..0000000 -- cgit