lb-rs 26.4.13

The rust library for interacting with your lockbook.
Documentation
use crate::Lb;
use crate::model::ValidationFailure;
use crate::model::errors::{LbErr, LbErrKind, LbResult};
use crate::model::file::File;
use crate::model::file_metadata::FileType;
use futures::StreamExt;
use futures::stream::FuturesUnordered;
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Path, PathBuf};
use uuid::Uuid;

pub enum ImportStatus {
    CalculatedTotal(usize),
    StartingItem(String),
    FinishedItem(File),
}

impl Lb {
    #[instrument(level = "debug", skip(self, update_status), err(Debug))]
    pub async fn import_files<F: Fn(ImportStatus)>(
        &self, sources: &[PathBuf], dest: Uuid, update_status: &F,
    ) -> LbResult<()> {
        update_status(ImportStatus::CalculatedTotal(get_total_child_count(sources)?));

        let parent = self.get_file_by_id(dest).await?;
        if !parent.is_folder() {
            return Err(LbErrKind::Validation(ValidationFailure::NonFolderWithChildren(dest)))?;
        }

        let import_file_futures = FuturesUnordered::new();

        for source in sources {
            let lb = self.clone();

            import_file_futures.push(async move {
                lb.import_file_recursively(source, dest, update_status)
                    .await
            });
        }

        import_file_futures
            .collect::<Vec<LbResult<()>>>()
            .await
            .into_iter()
            .collect::<LbResult<()>>()
    }

    async fn import_file_recursively<F: Fn(ImportStatus)>(
        &self, disk_path: &Path, dest: Uuid, update_status: &F,
    ) -> LbResult<()> {
        update_status(ImportStatus::StartingItem(format!("{}", disk_path.display())));

        if !disk_path.exists() {
            return Err(LbErrKind::DiskPathInvalid.into());
        }

        let name = disk_path
            .file_name()
            .and_then(|name| name.to_str())
            .ok_or(LbErrKind::DiskPathInvalid)?
            .to_string();

        let file_type = if disk_path.is_file() { FileType::Document } else { FileType::Folder };

        let mut tries = 0;
        let mut retry_name = name.clone();
        let file: File;

        loop {
            match self.create_file(&retry_name, &dest, file_type).await {
                Ok(new_file) => {
                    file = new_file;
                    break;
                }
                Err(err)
                    if matches!(
                        err.kind,
                        LbErrKind::Validation(ValidationFailure::PathConflict(_))
                    ) =>
                {
                    tries += 1;
                    retry_name = format!("{name}-{tries}");
                }
                Err(err) => return Err(err),
            }
        }

        match file_type {
            FileType::Document => {
                let content = fs::read(disk_path).map_err(LbErr::from)?;
                self.write_document(file.id, content.as_slice()).await?;

                update_status(ImportStatus::FinishedItem(file));
            }
            FileType::Folder => {
                let id = file.id;
                update_status(ImportStatus::FinishedItem(file));

                let disk_children = fs::read_dir(disk_path).map_err(LbErr::from)?;

                let import_file_futures = FuturesUnordered::new();

                for disk_child in disk_children {
                    let child_path = disk_child.map_err(LbErr::from)?.path();
                    let lb = self.clone();

                    import_file_futures.push(async move {
                        lb.import_file_recursively(&child_path, id, update_status)
                            .await
                    });
                }

                import_file_futures
                    .collect::<Vec<LbResult<()>>>()
                    .await
                    .into_iter()
                    .collect::<LbResult<()>>()?;
            }

            FileType::Link { .. } => {
                error!("links should not be interpreted!")
            }
        }

        Ok(())
    }

    #[instrument(level = "debug", skip(self, update_status), err(Debug))]
    pub async fn export_file<F: Fn(ExportFileInfo)>(
        &self, id: Uuid, dest: PathBuf, edit: bool, update_status: &Option<F>,
    ) -> LbResult<()> {
        if dest.is_file() {
            return Err(LbErrKind::DiskPathInvalid.into());
        }

        self.export_file_recursively(id, &dest, edit, update_status)
            .await
    }

    pub async fn export_file_recursively<F: Fn(ExportFileInfo)>(
        &self, id: Uuid, disk_path: &Path, edit: bool, update_status: &Option<F>,
    ) -> LbResult<()> {
        let file = self.get_file_by_id(id).await?;

        let new_dest = disk_path.join(&file.name);

        if let Some(update_status) = update_status {
            update_status(ExportFileInfo {
                disk_path: disk_path.to_path_buf(),
                lockbook_path: self.get_path_by_id(file.id).await?,
            });
        }

        match file.file_type {
            FileType::Document => {
                let mut disk_file = if edit {
                    OpenOptions::new()
                        .write(true)
                        .create(true)
                        .truncate(true)
                        .open(new_dest)
                } else {
                    OpenOptions::new()
                        .write(true)
                        .create_new(true)
                        .open(new_dest)
                }
                .map_err(LbErr::from)?;

                disk_file
                    .write(self.read_document(file.id, true).await?.as_slice())
                    .map_err(LbErr::from)?;
            }
            FileType::Folder => {
                fs::create_dir(new_dest.clone()).map_err(LbErr::from)?;
                let export_file_futures = FuturesUnordered::new();

                for child in self.get_children(&file.id).await? {
                    let lb = self.clone();
                    let new_dest = &new_dest;

                    export_file_futures.push(async move {
                        lb.export_file_recursively(child.id, new_dest, edit, update_status)
                            .await
                    });
                }

                export_file_futures
                    .collect::<Vec<LbResult<()>>>()
                    .await
                    .into_iter()
                    .collect::<LbResult<()>>()?;
            }
            FileType::Link { target } => {
                let export_file_futures = FuturesUnordered::new();
                let lb = self.clone();

                export_file_futures.push(async move {
                    lb.export_file_recursively(target, disk_path, edit, update_status)
                        .await
                });

                export_file_futures
                    .collect::<Vec<LbResult<()>>>()
                    .await
                    .into_iter()
                    .collect::<LbResult<()>>()?;
            }
        }

        Ok(())
    }
}

fn get_total_child_count(paths: &[PathBuf]) -> LbResult<usize> {
    let mut count = 0;
    for p in paths {
        count += get_child_count(p)?;
    }
    Ok(count)
}

fn get_child_count(path: &Path) -> LbResult<usize> {
    let mut count = 1;
    if path.is_dir() {
        let children = fs::read_dir(path).map_err(LbErr::from)?;
        for maybe_child in children {
            let child_path = maybe_child.map_err(LbErr::from)?.path();

            count += get_child_count(&child_path)?;
        }
    }
    Ok(count)
}

#[derive(Debug, Clone)]
pub struct ExportFileInfo {
    pub disk_path: PathBuf,
    pub lockbook_path: String,
}