Browse Source

Generic'fied the storage, so we can make the Joplin storage work

master
Julio Biason 5 years ago
parent
commit
e869ddf7ee
  1. 2
      .gitignore
  2. 2
      Cargo.lock
  3. 2
      Cargo.toml
  4. 60
      src/config/mod.rs
  5. 142
      src/main.rs
  6. 44
      src/storage/attachment.rs
  7. 79
      src/storage/filesystem.rs
  8. 3
      src/storage/mod.rs
  9. 12
      src/storage/storage.rs

2
.gitignore vendored

@ -10,3 +10,5 @@ mastodon.toml
downfav.toml downfav.toml
data data
functional.cafe
sinblr.com

2
Cargo.lock generated

@ -260,7 +260,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "downfav" name = "downfav"
version = "0.2.0" version = "0.3.0"
dependencies = [ dependencies = [
"elefren 0.19.4 (registry+https://github.com/rust-lang/crates.io-index)", "elefren 0.19.4 (registry+https://github.com/rust-lang/crates.io-index)",
"html2md 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)", "html2md 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",

2
Cargo.toml

@ -1,6 +1,6 @@
[package] [package]
name = "downfav" name = "downfav"
version = "0.2.0" version = "0.3.0"
authors = ["Julio Biason <julio.biason@pm.me>"] authors = ["Julio Biason <julio.biason@pm.me>"]
edition = "2018" edition = "2018"

60
src/config/mod.rs

@ -0,0 +1,60 @@
use std::fs::File;
use std::io::prelude::*;
use serde_derive::Deserialize;
use serde_derive::Serialize;
use toml;
#[derive(Serialize, Deserialize, Debug)]
pub struct JoplinConfig {
port: u32,
folder: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Config {
pub last_favorite: String,
pub joplin: Option<JoplinConfig>,
}
impl Config {
pub fn get() -> Config {
if let Ok(mut fp) = File::open("downfav.toml") {
let mut contents = String::new();
fp.read_to_string(&mut contents).unwrap();
let config: Config = toml::from_str(&contents).unwrap_or(Config {
last_favorite: "".to_string(),
joplin: None,
});
config
} else {
Config {
last_favorite: "".to_string(),
joplin: None,
}
}
}
pub fn save(&self, most_recent_favourite: Option<String>) -> () {
if let Some(id) = most_recent_favourite {
let new_configuration = Config {
last_favorite: id,
joplin: match &self.joplin {
None => None,
Some(x) => Some(JoplinConfig {
folder: x.folder.to_string(),
port: x.port,
}),
},
};
let content = toml::to_string(&new_configuration).unwrap();
if let Ok(mut fp) = File::create("downfav.toml") {
fp.write_all(content.as_bytes()).unwrap();
}
}
}
}

142
src/main.rs

@ -1,38 +1,17 @@
use std::fs::File;
use std::io; use std::io;
use std::io::prelude::*;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;
use elefren::entities::attachment::Attachment; use crate::storage::storage::Storage;
use elefren::entities::status::Status;
use elefren::helpers::cli; use elefren::helpers::cli;
use elefren::helpers::toml as elefren_toml; use elefren::helpers::toml as elefren_toml;
use elefren::prelude::*; use elefren::prelude::*;
use reqwest; mod config;
mod storage;
use serde_derive::Deserialize;
use serde_derive::Serialize;
use toml;
#[derive(Serialize, Deserialize, Debug)]
struct JoplinConfig {
port: u32,
folder: String,
}
#[derive(Serialize, Deserialize, Debug)]
struct Config {
last_favorite: String,
joplin: Option<JoplinConfig>,
}
fn main() { fn main() {
let config = dbg!(get_config()); let config = dbg!(config::Config::get());
let client = get_mastodon_connection(); let client = dbg!(get_mastodon_connection());
let top = config.last_favorite.to_string(); let top = dbg!(config.last_favorite.to_string());
let most_recent_favourite = client let most_recent_favourite = client
.favourites() .favourites()
@ -40,7 +19,9 @@ fn main() {
.items_iter() .items_iter()
.take_while(|record| record.id != top) .take_while(|record| record.id != top)
.map(|record| { .map(|record| {
dump_record(&record); let storage = dbg!(storage::filesystem::Filesystem::from(&record));
storage.open();
storage.save();
record record
}) })
.fold(None, { .fold(None, {
@ -53,34 +34,16 @@ fn main() {
} }
}); });
save_config(&config, most_recent_favourite) config.save(most_recent_favourite);
}
fn save_config(config: &Config, most_recent_favourite: Option<String>) -> () {
if let Some(id) = most_recent_favourite {
let new_configuration = Config {
last_favorite: id,
joplin: match &config.joplin {
None => None,
Some(x) => Some(JoplinConfig {
folder: x.folder.to_string(),
port: x.port,
}),
},
};
let content = toml::to_string(&new_configuration).unwrap();
if let Ok(mut fp) = File::create("downfav.toml") {
fp.write_all(content.as_bytes()).unwrap();
}
}
} }
/// Get a connection with Mastodon; if there is no set up with any account yet,
/// requests one.
fn get_mastodon_connection() -> Mastodon { fn get_mastodon_connection() -> Mastodon {
if let Ok(data) = elefren_toml::from_file("mastodon.toml") { if let Ok(data) = elefren_toml::from_file("mastodon.toml") {
Mastodon::from(data) Mastodon::from(data)
} else { } else {
print!("Your server URL: "); println!("Your server URL: ");
let mut server = String::new(); let mut server = String::new();
io::stdin() io::stdin()
.read_line(&mut server) .read_line(&mut server)
@ -95,82 +58,3 @@ fn get_mastodon_connection() -> Mastodon {
mastodon mastodon
} }
} }
fn get_config() -> Config {
if let Ok(mut fp) = File::open("downfav.toml") {
let mut contents = String::new();
fp.read_to_string(&mut contents).unwrap();
let config: Config = toml::from_str(&contents).unwrap_or(Config {
last_favorite: "".to_string(),
joplin: None,
});
config
} else {
Config {
last_favorite: "".to_string(),
joplin: None,
}
}
}
fn dump_record(record: &Status) {
println!("Downloading {}/{}", &record.account.acct, &record.id);
create_structure(dbg!(&record));
save_content(&record);
save_attachments(&record);
}
fn toot_dir(record: &Status) -> PathBuf {
Path::new("data")
.join(&record.account.acct)
.join(&record.id)
}
fn create_structure(record: &Status) {
println!("Current ID: {}", record.id);
std::fs::create_dir_all(toot_dir(record)).expect("Failed to create the storage path");
}
fn save_content(record: &Status) {
if let Ok(mut fp) = File::create(toot_dir(&record).join("toot.md")) {
fp.write_all(html2md::parse_html(&record.content).as_bytes())
.expect("Failed to save content");
}
}
fn save_attachments(record: &Status) {
let base_path = toot_dir(&record);
record
.media_attachments
.iter()
.for_each(move |x| save_attachment(dbg!(&x), &base_path));
}
fn save_attachment(attachment: &Attachment, base_path: &PathBuf) {
let filename = get_attachment_filename(dbg!(&attachment.url));
println!("\tAttachment: {:?}", &filename);
let saving_target = dbg!(base_path.join(filename));
if let Ok(mut fp) = File::create(saving_target) {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(600))
.build()
.unwrap();
client
.get(&attachment.url)
.send()
.expect("Failed to connect to server")
.copy_to(&mut fp)
.expect("Failed to save attachment");
}
}
fn get_attachment_filename(url: &str) -> String {
let mut frags = url.rsplitn(2, '/');
if let Some(path_part) = frags.next() {
dbg!(path_part.split('?').next().unwrap_or(url).to_string())
} else {
// this is, most of the time, bad (due special characters -- like '?' -- and path)
dbg!(url.to_string())
}
}

44
src/storage/attachment.rs

@ -0,0 +1,44 @@
use std::convert::From;
use std::fs::File;
use std::path::Path;
use std::time::Duration;
#[derive(Debug)]
pub struct Attachment {
url: String,
}
impl From<&elefren::entities::attachment::Attachment> for Attachment {
fn from(origin: &elefren::entities::attachment::Attachment) -> Self {
println!("Found attachment: {}", origin.url);
Self {
url: origin.url.to_string(),
}
}
}
impl Attachment {
pub fn get_filename(&self) -> String {
let mut frags = self.url.rsplitn(2, '/');
if let Some(path_part) = frags.next() {
dbg!(path_part.split('?').next().unwrap_or(&self.url).to_string())
} else {
// this is, most of the time, bad (due special characters -- like '?' -- and path)
dbg!(self.url.to_string())
}
}
pub fn download(&self, local_filename: &Path) {
let mut fp = File::create(local_filename).expect("Failed to create file");
reqwest::Client::builder()
.timeout(Duration::from_secs(600))
.build()
.unwrap()
.get(&self.url)
.send()
.unwrap()
.copy_to(&mut fp)
.unwrap();
}
}

79
src/storage/filesystem.rs

@ -0,0 +1,79 @@
use elefren::entities::status::Status;
use html2md;
use std::convert::From;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use crate::storage::attachment::Attachment;
use crate::storage::storage::Storage;
#[derive(Debug)]
pub struct Filesystem {
id: String,
account: String,
text: String,
attachments: Vec<Attachment>,
}
impl Filesystem {
/// The directory in which the data from this toot will be saved.
fn dir(&self) -> PathBuf {
Path::new("data").join(&self.account).join(&self.id)
}
/// Make sure the path structure exists for saving the data.
fn create_dirs(&self) {
std::fs::create_dir_all(self.dir()).expect("Failed to create storage directory");
}
/// Save the content in the directory.
fn save_content(&self) {
let filename = self.dir().join("toot.md");
let mut fp = File::create(filename).expect("Failed to create file");
fp.write_all(self.text.as_bytes())
.expect("Failed to save content");
}
/// Save the attachments.
fn save_attachments(&self) {
self.attachments.iter().for_each(|attachment| {
let filename = self.dir().join(attachment.get_filename());
attachment.download(filename.as_path());
})
}
}
impl Storage for Filesystem {
fn open(&self) {
dbg!(self.create_dirs());
}
fn get_id(&self) -> &String {
&self.id
}
fn save(&self) {
self.save_content();
self.save_attachments();
}
}
impl From<&Status> for Filesystem {
fn from(origin: &Status) -> Self {
println!("Downloading ID: {}", origin.id);
Self {
id: origin.id.to_string(),
account: origin.account.acct.to_string(),
text: html2md::parse_html(&origin.content),
// on save, we download those URLs
attachments: origin
.media_attachments
.iter()
.map(|attachment| Attachment::from(attachment))
.collect(),
}
}
}

3
src/storage/mod.rs

@ -0,0 +1,3 @@
pub mod filesystem;
pub mod storage;
pub mod attachment;

12
src/storage/storage.rs

@ -0,0 +1,12 @@
/// Trait for storing favorites on a storage.
pub trait Storage {
/// Initization. Any required pre-storage functions must be added here.
fn open(&self);
/// Save the favourite in the storage.
fn save(&self);
/// Return the original favourite identification.
fn get_id(&self) -> &String;
}
Loading…
Cancel
Save