clync 0.4.1

Encrypted sync for Claude Code across machines
use anyhow::Result;
use std::path::{Path, PathBuf};

use super::{LocalFs, Store};

pub struct FolderStore {
    fs: LocalFs,
}

impl FolderStore {
    pub fn new(path: PathBuf) -> Self {
        Self {
            fs: LocalFs::new(path),
        }
    }

    #[allow(dead_code)]
    pub fn init(path: &Path) -> Result<Self> {
        std::fs::create_dir_all(path)?;
        Ok(Self::new(path.to_path_buf()))
    }

    #[allow(dead_code)]
    pub fn root(&self) -> &Path {
        self.fs.root()
    }
}

impl Store for FolderStore {
    fn write_file(&self, rel_path: &str, data: &[u8]) -> Result<()> {
        self.fs.write_file(rel_path, data)
    }

    fn read_file(&self, rel_path: &str) -> Result<Vec<u8>> {
        self.fs.read_file(rel_path)
    }

    fn exists(&self, rel_path: &str) -> bool {
        self.fs.exists(rel_path)
    }

    fn list_files(&self, prefix: &str) -> Result<Vec<String>> {
        self.fs.list_files(prefix)
    }

    fn file_size(&self, rel_path: &str) -> Result<u64> {
        self.fs.file_size(rel_path)
    }

    fn atomic_write(&self, rel_path: &str, data: &[u8]) -> Result<()> {
        self.fs.atomic_write(rel_path, data)
    }

    fn sync_down(&self) -> Result<()> {
        Ok(())
    }

    fn sync_up(&self, _message: &str) -> Result<()> {
        Ok(())
    }

    fn lock(&self) -> Result<Box<dyn std::any::Any>> {
        self.fs.lock()
    }

    fn try_lock(&self) -> Result<Box<dyn std::any::Any>> {
        self.fs.try_lock()
    }

    fn local_path(&self) -> Option<&Path> {
        Some(self.fs.root())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn temp_dir(name: &str) -> PathBuf {
        let dir = std::env::temp_dir()
            .join("clync-folder-store-test")
            .join(name)
            .join(format!("{}", std::process::id()));
        if dir.exists() {
            std::fs::remove_dir_all(&dir).ok();
        }
        std::fs::create_dir_all(&dir).unwrap();
        dir
    }

    #[test]
    fn init_creates_dir() {
        let dir = temp_dir("init");
        let path = dir.join("store");
        let store = FolderStore::init(&path).unwrap();
        assert!(path.exists());
        assert_eq!(store.root(), path);
        assert!(!path.join(".git").exists());
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn write_read_exists() {
        let dir = temp_dir("write_read");
        let store = FolderStore::init(&dir.join("store")).unwrap();
        store.write_file("test.txt", b"hello").unwrap();
        assert!(store.exists("test.txt"));
        assert!(!store.exists("missing.txt"));
        let data = store.read_file("test.txt").unwrap();
        assert_eq!(data, b"hello");
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn list_files_works() {
        let dir = temp_dir("list_files");
        let store = FolderStore::init(&dir.join("store")).unwrap();
        store.write_file("sessions/a.txt", b"a").unwrap();
        store.write_file("sessions/b.txt", b"b").unwrap();
        let files = store.list_files("sessions").unwrap();
        assert_eq!(files.len(), 2);
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn sync_up_down_are_noops() {
        let dir = temp_dir("sync_noop");
        let store = FolderStore::init(&dir.join("store")).unwrap();
        store.sync_up("test").unwrap();
        store.sync_down().unwrap();
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn lock_works() {
        let dir = temp_dir("lock");
        let store = FolderStore::init(&dir.join("store")).unwrap();
        let _lock = store.lock().unwrap();
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn atomic_write_works() {
        let dir = temp_dir("atomic");
        let store = FolderStore::init(&dir.join("store")).unwrap();
        store.atomic_write("test.txt", b"data").unwrap();
        assert_eq!(store.read_file("test.txt").unwrap(), b"data");
        assert!(!store.exists("test.txt.tmp"));
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn file_size_works() {
        let dir = temp_dir("file_size");
        let store = FolderStore::init(&dir.join("store")).unwrap();
        store.write_file("test.txt", b"12345").unwrap();
        assert_eq!(store.file_size("test.txt").unwrap(), 5);
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn lock_creates_lock_file() {
        let dir = temp_dir("lock_creates");
        let store = FolderStore::init(&dir.join("store")).unwrap();
        let _lock = store.lock().unwrap();
        assert!(dir.join("store").join(".clync.lock").exists());
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn try_lock_fails_when_already_locked() {
        let dir = temp_dir("try_lock_fail");
        let store = FolderStore::init(&dir.join("store")).unwrap();
        let _lock = store.lock().unwrap();
        let result = store.try_lock();
        assert!(result.is_err());
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn try_lock_succeeds_when_unlocked() {
        let dir = temp_dir("try_lock_ok");
        let store = FolderStore::init(&dir.join("store")).unwrap();
        let _lock = store.try_lock().unwrap();
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn lock_released_after_drop() {
        let dir = temp_dir("lock_drop");
        let store = FolderStore::init(&dir.join("store")).unwrap();
        {
            let _lock = store.lock().unwrap();
        }
        // After drop, try_lock should succeed
        let _lock = store.try_lock().unwrap();
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn local_path_returns_root() {
        let dir = temp_dir("local_path");
        let store_path = dir.join("store");
        let store = FolderStore::init(&store_path).unwrap();
        assert_eq!(store.local_path(), Some(store_path.as_path()));
        std::fs::remove_dir_all(&dir).ok();
    }
}