initial
This commit is contained in:
commit
7f0ab1fc9a
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/target
|
||||
.env
|
||||
target/
|
||||
dest/
|
1146
Cargo.lock
generated
Normal file
1146
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
Cargo.toml
Normal file
23
Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "pull-articles"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
git2 = { version = "0.16.1" }
|
||||
clap = "4.2.1"
|
||||
color-eyre = "0.6.2"
|
||||
dotenv = "0.15.0"
|
||||
eyre = "0.6.8"
|
||||
serde = { version = "1.0.159", features = ["derive"] }
|
||||
serde_json = "1.0.95"
|
||||
serde_yaml = "0.9.19"
|
||||
tempdir = "0.3.7"
|
||||
tokio = { version = "1.27.0", features = ["full"] }
|
||||
tracing = { version = "0.1.37", features = ["log"] }
|
||||
tracing-subscriber = "0.3.16"
|
||||
serde_toml = "0.0.1"
|
||||
toml = { version = "0.7.3", features = ["preserve_order"] }
|
||||
tokio-scoped = "0.2.0"
|
202
src/main.rs
Normal file
202
src/main.rs
Normal file
@ -0,0 +1,202 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use git2::build::{CheckoutBuilder, RepoBuilder};
|
||||
use git2::{Cred, FetchOptions, RemoteCallbacks};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tempdir::TempDir;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> eyre::Result<()> {
|
||||
let _ = dotenv::dotenv();
|
||||
color_eyre::install().unwrap();
|
||||
tracing_subscriber::fmt().pretty().init();
|
||||
|
||||
let matches = clap::Command::new("pull-articles")
|
||||
.arg(clap::Arg::new("repo").long("repo").required(true))
|
||||
.arg(clap::Arg::new("path").long("path").required(true))
|
||||
.arg(clap::Arg::new("out").long("out").required(true))
|
||||
.get_matches();
|
||||
|
||||
let repo = matches.get_one::<String>("repo").unwrap();
|
||||
let path = matches.get_one::<String>("path").unwrap();
|
||||
let out = matches.get_one::<String>("out").unwrap();
|
||||
|
||||
tracing::info!(repo = repo, path = path, out = out, "pulling articles");
|
||||
|
||||
let tmpdir = TempDir::new("pull-articles")?;
|
||||
let tmpdir = tmpdir.path();
|
||||
|
||||
tracing::info!(
|
||||
repo = repo,
|
||||
dest_dir = tmpdir.display().to_string(),
|
||||
"clone repo"
|
||||
);
|
||||
|
||||
let mut cb = RemoteCallbacks::new();
|
||||
cb.credentials(|_, _, _| {
|
||||
let username = std::env::var("GIT_USERNAME").expect("GIT_USERNAME to be set");
|
||||
let password = std::env::var("GIT_PASSWORD").expect("GIT_PASSWORD to be set");
|
||||
Cred::userpass_plaintext(&username, &password)
|
||||
});
|
||||
let co = CheckoutBuilder::new();
|
||||
let mut fo = FetchOptions::new();
|
||||
fo.remote_callbacks(cb);
|
||||
|
||||
let mut repo_dir = tmpdir.to_path_buf();
|
||||
repo_dir.push("repo");
|
||||
|
||||
RepoBuilder::new()
|
||||
.fetch_options(fo)
|
||||
.with_checkout(co)
|
||||
.clone(repo, &repo_dir)?;
|
||||
|
||||
let mut repo_dir = repo_dir.clone();
|
||||
repo_dir.push(path);
|
||||
tracing::info!(repo_dir = repo_dir.display().to_string(), "reading files");
|
||||
|
||||
let out_dir = PathBuf::from(out);
|
||||
tokio::fs::create_dir_all(&out_dir).await?;
|
||||
let mut dir = tokio::fs::read_dir(repo_dir).await?;
|
||||
|
||||
while let Some(file) = dir.next_entry().await? {
|
||||
if let Ok(ft) = file.file_type().await {
|
||||
if ft.is_file() {
|
||||
let file_content = tokio::fs::read(file.path()).await?;
|
||||
let file_str = std::str::from_utf8(file_content.as_slice())?;
|
||||
|
||||
let (frontmatter, content) = extract_frontmatter(file_str)?;
|
||||
let transformed_frontmatter = transform_frontmatter(frontmatter)?;
|
||||
|
||||
let new_article = format!("{}\n{}", transformed_frontmatter, content);
|
||||
let mut out_file = out_dir.clone();
|
||||
out_file.push(file.file_name());
|
||||
|
||||
tokio::fs::write(out_file, new_article).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_frontmatter(content: impl Into<String>) -> eyre::Result<(String, String)> {
|
||||
let content: String = content.into();
|
||||
|
||||
let start_marker = content
|
||||
.find("---")
|
||||
.ok_or(eyre::anyhow!("could not find start ---"))?;
|
||||
let content = &content[start_marker + 4..];
|
||||
let end_marker = content
|
||||
.find("---")
|
||||
.ok_or(eyre::anyhow!("could not find start ---"))?;
|
||||
let frontmatter = &content[start_marker..end_marker];
|
||||
let rest = &content[end_marker + 4..];
|
||||
|
||||
Ok((frontmatter.to_string(), rest.to_string()))
|
||||
}
|
||||
|
||||
fn transform_frontmatter(frontmatter: String) -> eyre::Result<String> {
|
||||
let obsidian_post: ObsidianPost = serde_yaml::from_str(&frontmatter)?;
|
||||
|
||||
let zola_post = ZolaPost {
|
||||
title: obsidian_post.title,
|
||||
description: Some(obsidian_post.description),
|
||||
date: obsidian_post.created,
|
||||
updated: obsidian_post
|
||||
.updates
|
||||
.map(|u| u.last().map(|u| u.time.clone()))
|
||||
.flatten(),
|
||||
draft: obsidian_post.hidden,
|
||||
slug: obsidian_post.slug,
|
||||
authors: Some(vec!["kjuulh".into()]),
|
||||
};
|
||||
|
||||
let transformed_frontmatter = toml::to_string(&zola_post)?;
|
||||
|
||||
Ok(format!("+++\n{transformed_frontmatter}+++"))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
struct ObsidianPostChange {
|
||||
time: String,
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
struct ObsidianPost {
|
||||
#[serde(rename(serialize = "type", deserialize = "type"))]
|
||||
blog_type: String,
|
||||
title: String,
|
||||
description: String,
|
||||
hidden: Option<bool>,
|
||||
created: String,
|
||||
updates: Option<Vec<ObsidianPostChange>>,
|
||||
tags: Vec<String>,
|
||||
slug: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
struct ZolaPost {
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
date: String,
|
||||
updated: Option<String>,
|
||||
draft: Option<bool>,
|
||||
slug: Option<String>,
|
||||
authors: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{extract_frontmatter, transform_frontmatter};
|
||||
|
||||
#[test]
|
||||
fn can_extract_frontmatter_from_article() -> eyre::Result<()> {
|
||||
let article = r#"---
|
||||
type: "blog-post"
|
||||
title: "Advancing career"
|
||||
description: "2023-04-01-advanding-career"
|
||||
hidden: true
|
||||
created: "2023-04-01"
|
||||
updates:
|
||||
- time: "2023-04-01"
|
||||
description: "first iteration"
|
||||
tags:
|
||||
- '#blog'
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
My current conundrum is figuring out how I want to advance my career, what directions to take, and what options to look out for.
|
||||
|
||||
In the start of my career and during my studies I wanted to figure out what kind of work I enjoyed and gave me energy, and also what I am most suited for. None of these are of course set in stone, and I don't believe there is a perfect choice, but that is neither here nor there.
|
||||
"#;
|
||||
|
||||
let actual = extract_frontmatter(article)?;
|
||||
|
||||
assert_eq!(actual.0, "type: \"blog-post\"\ntitle: \"Advancing career\"\ndescription: \"2023-04-01-advanding-career\"\nhidden: true\ncreated: \"2023-04-01\"\nupdates:\n- time: \"2023-04-01\"\n description: \"first iteration\"\ntags:\n- '#blog'\n");
|
||||
assert_eq!(actual.1, "\n# Introduction\n\nMy current conundrum is figuring out how I want to advance my career, what directions to take, and what options to look out for. \n\nIn the start of my career and during my studies I wanted to figure out what kind of work I enjoyed and gave me energy, and also what I am most suited for. None of these are of course set in stone, and I don't believe there is a perfect choice, but that is neither here nor there.\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_transform_frontmatter() -> eyre::Result<()> {
|
||||
let frontmatter = r#"type: "blog-post"
|
||||
title: "Advancing career"
|
||||
description: "2023-04-01-advanding-career"
|
||||
hidden: true
|
||||
created: "2023-04-01"
|
||||
updates:
|
||||
- time: "2023-04-01"
|
||||
description: "first iteration"
|
||||
tags:
|
||||
- '#blog'"#;
|
||||
|
||||
let res = transform_frontmatter(frontmatter.into())?;
|
||||
|
||||
assert_eq!(&res, "+++\ntitle = \"Advancing career\"\ndescription = \"2023-04-01-advanding-career\"\ndate = \"2023-04-01\"\nupdated = \"2023-04-01\"\ndraft = true\nauthors = [\"kjuulh\"]\n\n+++\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user