ogk 2.2.1

cli tool for open.go.kr
use crate::client::{BillWithFiles, Client, DntcFile, DtlVo};
use crate::utils::auth::AuthUser;
use crate::utils::{config, date};
use async_trait::async_trait;
use bytes::Bytes;
use chrono::prelude::Utc;
use console::Emoji;
use dirs::home_dir;
use git2::{
    self, Commit, Cred, IndexAddOption, ObjectType, Oid, RemoteCallbacks, Repository, Signature,
};
use regex::Regex;
use std::error::Error;
use std::fs::{create_dir, remove_dir_all, File};
use std::io;
use std::path::Path;

static DOCUMENT: Emoji<'_, '_> = Emoji("📑  ", "");

pub struct FileManager<'a> {
    _auth_user: &'a AuthUser,
    _remote_url: String,
    _local_path: String,
    _local_repo: Option<Repository>,
    _git_signature: Signature<'a>,
}

impl<'a> FileManager<'a> {
    pub async fn new(auth_user: &'a AuthUser) -> Result<FileManager<'a>, Box<dyn Error>> {
        let global_config = git2::Config::open_default().unwrap();

        let _local_path = auth_user
            .local_repository
            .as_ref()
            .unwrap()
            .clone()
            .to_string();
        let _remote_url = auth_user
            .remote_repository
            .as_ref()
            .unwrap()
            .clone()
            .to_string();

        let mut fm = FileManager {
            _auth_user: auth_user,
            _local_path,
            _remote_url,
            _local_repo: None,
            _git_signature: Signature::now(
                &global_config.get_string("user.name").unwrap(),
                &global_config.get_string("user.email").unwrap(),
            )
            .unwrap(),
        };

        if !Path::new(&fm._local_path).exists() {
            fm.clone_remote_repo();
        }

        Ok(fm)
    }

    pub fn clone_remote_repo(&mut self) -> &Option<Repository> {
        let _ = remove_dir_all(&self._local_path);

        let mut callbacks = RemoteCallbacks::new();
        callbacks.credentials(|_url, username_from_url, _allowed_types| {
            Cred::ssh_key(
                username_from_url.unwrap(),
                None,
                // TODO: ssh key 관리 고려
                std::path::Path::new(&format!(
                    "{}/.ssh/id_ed25519",
                    home_dir().unwrap().to_str().unwrap()
                )),
                None,
            )
        });

        let mut fo = git2::FetchOptions::new();
        fo.remote_callbacks(callbacks);

        let mut builder = git2::build::RepoBuilder::new();
        builder.fetch_options(fo);

        match builder.clone(&self._remote_url, Path::new(&self._local_path)) {
            Ok(repo) => {
                self._local_repo = Some(repo);
            }
            Err(error) => {
                println!("{}", &self._remote_url);
                println!("{}", &self._local_path);
                panic!("{}", error);
            }
        }

        &self._local_repo
    }

    pub async fn download(
        &self,
        auth_user: &AuthUser,
        client: &Client,
        bill: &BillWithFiles,
        bill_from_list: &DtlVo,
    ) -> Result<Option<Vec<DntcFile>>, Box<dyn std::error::Error>> {
        let config = config::Config::load_or_new()?;
        match config.remote_file_repository {
            Some(_) => {
                let mut downloaded_files: Vec<DntcFile> = vec![];
                let fm = FileManager::new(auth_user).await.unwrap();

                if let Some(ref file_list) = bill.atchFileList {
                    for file in &*file_list {
                        if fm.has_downloaded(bill, bill_from_list, &file.uploadFileOrginlNm)
                            == false
                        {
                            let downloaded = client.download_file(file).await?;
                            let _ = fm.save(
                                &downloaded,
                                bill,
                                bill_from_list,
                                &file.uploadFileOrginlNm,
                            );
                            downloaded_files.push(file.clone());
                        }
                    }

                    Ok(Some(downloaded_files))
                } else {
                    Ok(Some(downloaded_files))
                }
            }
            None => {
                eprintln!("청구파일을 다운로드 하려면 원격저장소 주소를 먼저 설정해주세요.");
                Ok(None)
            }
        }
    }

    // {접수일자}_{청구_제묵}
    pub fn make_dirname(request_date: &str, request_subject: &str) -> String {
        let re_illegal_symbols = Regex::new("[,<>\"\n \t()\'?~\u{1c}]").unwrap();
        let re_retouch = Regex::new("_+").unwrap();
        format!(
            "{}_{}",
            request_date.replace(".", "-"),
            re_retouch
                .replace_all(
                    &re_illegal_symbols.replace_all(request_subject.trim(), "_"),
                    "_",
                )
                .to_string()
        )
    }

    // {접수번호}_{처리기관이름}_{업로드_파일명}
    pub fn make_filename(
        registration_number: &str,
        rqest_full_instt_name: &str,
        file_name: &str,
    ) -> String {
        let re_illegal_symbols = Regex::new("[<>,\"\n \t()\'?~\u{1c}]").unwrap();
        let re_retouch = Regex::new("_+").unwrap();

        format!(
            "{}_{}_{}",
            registration_number,
            rqest_full_instt_name.replace(" ", "_"),
            re_retouch
                .replace_all(&re_illegal_symbols.replace_all(file_name.trim(), "_"), "_",)
                .to_string()
        )
    }

    pub fn save(
        &self,
        downloaded_file: &Bytes,
        downloadable_bill: &BillWithFiles,
        bill_from_list: &DtlVo,
        orig_file_name: &str,
    ) -> Result<File, Box<dyn std::error::Error>> {
        let dir_path = format!("{}/{}", &self._local_path, downloadable_bill.get_dirname(),);
        let file_path = format!(
            "{}/{}",
            &dir_path,
            downloadable_bill.get_filename(&bill_from_list.prcsFullInsttNm, orig_file_name)
        );

        create_dir(Path::new(&dir_path)).unwrap_or_default();
        let mut local_file = File::create(&file_path)?;
        io::copy(&mut downloaded_file.as_ref(), &mut local_file)?;
        Ok(local_file)
    }

    fn has_downloaded<T: Downloadable>(
        &self,
        downloadable_bill: &T,
        bill_from_list: &DtlVo,
        orig_file_name: &str,
    ) -> bool {
        let dir_path = format!("{}/{}", &self._local_path, downloadable_bill.get_dirname(),);
        let file_path = format!(
            "{}/{}",
            &dir_path,
            downloadable_bill.get_filename(&bill_from_list.prcsFullInsttNm, orig_file_name)
        );

        return Path::new(&file_path).exists();
    }

    fn remote_callbaks(&self) -> RemoteCallbacks<'a> {
        let mut callbacks = RemoteCallbacks::new();
        callbacks.credentials(|_url, username_from_url, _allowed_types| {
            Cred::ssh_key(
                username_from_url.unwrap(),
                None,
                std::path::Path::new(&format!(
                    "{}/.ssh/id_ed25519",
                    home_dir().unwrap().to_str().unwrap()
                )),
                None,
            )
        });
        return callbacks;
    }

    pub async fn sync_with_remote(&self) -> Result<(), Box<dyn Error>> {
        let repo = match Repository::open(&self._local_path) {
            Ok(repo) => repo,
            Err(e) => panic!("파일 저장소를 불러오는데 실패하였습니다.: {}", e),
        };

        let callbacks = self.remote_callbaks();
        let mut po = git2::FetchOptions::new();
        let mut po = po.remote_callbacks(callbacks);

        repo.find_remote("origin")?
            .fetch(&["main"], Some(&mut po), None);
        let fetch_head = repo.find_reference("FETCH_HEAD")?;
        let fetch_commit = repo.reference_to_annotated_commit(&fetch_head)?;
        let analysis = repo.merge_analysis(&[&fetch_commit])?;
        if analysis.0.is_up_to_date() {
            Ok(())
        } else {
            let refname = format!("refs/heads/main");
            let mut reference = repo.find_reference(&refname)?;
            reference.set_target(fetch_commit.id(), "Fast-Forward")?;
            repo.set_head(&refname)?;
            repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()));

            Ok(())
        }
    }

    pub async fn upload(&self) -> Result<Oid, git2::Error> {
        let callbacks = self.remote_callbaks();
        fn find_last_commit(repo: &Repository) -> Result<Commit, git2::Error> {
            let obj = repo.head()?.resolve()?.peel(ObjectType::Commit)?;
            obj.into_commit()
                .map_err(|_| git2::Error::from_str("Couldn't find commit"))
        }

        let repo = match Repository::open(&self._local_path) {
            Ok(repo) => repo,
            Err(e) => panic!("failed to open: {}", e),
        };

        let mut index = repo.index().unwrap();
        let _ = index.add_all(["*"].iter(), IndexAddOption::DEFAULT, None);
        let _ = index.write();
        let oid = index.write_tree().unwrap();
        let parent_commit = find_last_commit(&repo).unwrap();
        let tree = repo.find_tree(oid).unwrap();

        repo.commit(
            Some("HEAD"),
            &self._git_signature,
            &self._git_signature,
            &format!(
                "{} - {}",
                DOCUMENT,
                date::KstDateTime::from(Utc::now()).format(Some("%F %T"))
            ),
            &tree,
            &[&parent_commit],
        )
        .unwrap();

        let mut remote = match repo.find_remote("origin") {
            Ok(r) => r,
            Err(_) => repo.remote("origin", &self._remote_url)?,
        };

        let mut po = git2::PushOptions::new();
        let mut po = po.remote_callbacks(callbacks);

        match remote.push(
            &["refs/heads/main", "refs/remotes/origin/main"],
            Some(&mut po),
        ) {
            Ok(_) => Ok(oid),
            Err(e) => {
                eprintln!("{}", e);
                Ok(oid)
            }
        }
    }
}

#[async_trait]
pub trait Downloadable {
    fn get_filename(&self, prcs_full_instt_nm: &str, orig_file_name: &str) -> String;
    fn get_dirname(&self) -> String;
}

#[cfg(test)]
mod tests {
    use crate::{files::FileManager, utils::auth::AuthConfig};

    #[tokio::test]
    async fn test_sync_with_remote() {
        let auth_config = AuthConfig::load_or_new().unwrap();
        let auth_user = &auth_config.find_org("default").unwrap().borrow();
        let fm = FileManager::new(auth_user).await.unwrap();
        fm.sync_with_remote().await;
    }
}