use super::{model::Thing, Error};
use crate::{
model::{
hosting_provider_id::HostingProviderId, hosting_type::HostingType,
hosting_unit_id::HostingUnitId, project::Project,
},
settings::PartialSettings,
tools::{SpdxLicenseExpression, LICENSE_UNKNOWN},
};
use async_std::{
fs::{self, File},
io,
path::{Path, PathBuf},
sync,
};
use async_stream::stream;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use core::slice;
use futures::{stream::BoxStream, stream::StreamExt};
use governor::{Quota, RateLimiter};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::sync::LazyLock;
use std::{
borrow::Cow,
collections::{BTreeSet, HashMap, HashSet, VecDeque},
fmt::Display,
sync::Arc,
};
use strum::{EnumIter, IntoEnumIterator};
use tokio::time::Duration;
use tracing::instrument;
pub const DEFAULT_SLICE_SIZE: ThingId = 1000;
pub const MIN_SLICE_SIZE: ThingId = 100;
pub const LAST_SCRAPE_FILE_NAME: &str = "last_scrape.csv";
pub type ThingId = u32;
const fn earliest() -> DateTime<Utc> {
DateTime::from_timestamp_nanos(0)
}
async fn ensure_dir_exists<P: AsRef<Path>>(dir: P) -> io::Result<()> {
if !dir.as_ref().exists().await {
fs::create_dir_all(dir.as_ref()).await?;
}
Ok(())
}
fn construct_file_path<P: AsRef<Path>, S: AsRef<str>>(dir: P, file_name: S, temp: bool) -> PathBuf {
if temp {
dir.as_ref()
.join(format!("{file_name}.temp", file_name = file_name.as_ref()))
} else {
dir.as_ref().join(file_name.as_ref())
}
}
fn last_scrape_file<P: AsRef<Path>>(dir: P, temp: bool) -> PathBuf {
construct_file_path(dir, LAST_SCRAPE_FILE_NAME, temp)
}
async fn write_last_scrape<P: AsRef<Path> + Send + Sync>(
temp_file: P,
file: P,
last_scrape: DateTime<Utc>,
) -> io::Result<()> {
let date_str: String = last_scrape.to_rfc3339();
fs::write(&temp_file, date_str).await?;
fs::rename(temp_file, file).await?;
Ok(())
}
async fn read_last_scrape<P: AsRef<Path>>(file: P) -> io::Result<Option<DateTime<Utc>>> {
if file.as_ref().exists().await {
let date_str = fs::read_to_string(file.as_ref()).await?;
Ok(Some(
DateTime::parse_from_rfc3339(&date_str)
.map_err(|parse_err| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to parse last scrape date ({date_str}): {parse_err}"),
)
})?
.into(),
))
} else {
Ok(None)
}
}
#[derive(
Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, EnumIter,
)]
pub enum ThingState {
FailedToFetch,
FailedToParse,
DoesNotExist,
Banned,
Proprietary,
OpenSource,
Untried,
}
impl ThingState {
#[must_use]
pub const fn has_content(self) -> bool {
match self {
Self::FailedToFetch
| Self::DoesNotExist
| Self::Banned
| Self::Proprietary
| Self::Untried => false,
Self::FailedToParse | Self::OpenSource => true,
}
}
#[must_use]
pub const fn to_str(self) -> &'static str {
match self {
Self::FailedToFetch => "failed_to_fetch",
Self::FailedToParse => "failed_to_parse",
Self::DoesNotExist => "does_not_exist",
Self::Banned => "banned",
Self::Proprietary => "proprietary",
Self::OpenSource => "open_source",
Self::Untried => "untried",
}
}
const fn is_successful_fetch(self) -> bool {
match self {
Self::FailedToFetch
| Self::FailedToParse
| Self::DoesNotExist
| Self::Banned
| Self::Untried => false,
Self::Proprietary | Self::OpenSource => true,
}
}
const fn output_value(self) -> u8 {
match self {
Self::FailedToFetch | Self::DoesNotExist | Self::Banned | Self::Untried => 1,
Self::FailedToParse => 2,
Self::Proprietary => 3,
Self::OpenSource => 4,
}
}
#[must_use]
pub const fn prefer_new_output_over_old(self, old: Self) -> bool {
self.output_value() >= old.output_value()
}
}
impl Display for ThingState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.to_str().fmt(f)
}
}
#[derive(Serialize, Deserialize, Debug, Eq)]
pub struct ThingMeta {
id: ThingId,
state: ThingState,
#[serde(default)]
first_scrape: Option<DateTime<Utc>>,
#[serde(default)]
last_scrape: Option<DateTime<Utc>>,
#[serde(default)]
last_successful_scrape: Option<DateTime<Utc>>,
#[serde(default)]
last_change: Option<DateTime<Utc>>,
#[serde(default)]
attempted_scrapes: usize,
scraped_changes: usize,
}
impl ThingMeta {
#[must_use]
pub const fn new(id: ThingId, state: ThingState, first_scrape: DateTime<Utc>) -> Self {
Self {
id,
state,
first_scrape: Some(first_scrape),
last_scrape: None,
last_successful_scrape: if state.is_successful_fetch() {
Some(first_scrape)
} else {
None
},
last_change: None,
attempted_scrapes: 1,
scraped_changes: 0,
}
}
pub fn normalize(&mut self) {
let earliest = earliest();
if let Some(first_scrape) = self.first_scrape {
if first_scrape == earliest {
self.first_scrape = None;
}
}
if let Some(last_scrape) = self.last_scrape {
if last_scrape == earliest {
self.last_scrape = None;
}
}
if let Some(last_change) = self.last_change {
if last_change == earliest {
self.last_change = None;
}
}
}
const fn new_untried(id: ThingId) -> Self {
Self::new(id, ThingState::Untried, earliest())
}
#[must_use]
pub const fn get_id(&self) -> ThingId {
self.id
}
}
impl PartialEq for ThingMeta {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Ord for ThingMeta {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match self.state.cmp(&other.state) {
core::cmp::Ordering::Equal => {}
ord @ (core::cmp::Ordering::Less | core::cmp::Ordering::Greater) => return ord,
}
self.last_scrape.cmp(&other.last_scrape)
}
}
impl PartialOrd for ThingMeta {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
pub struct ThingStoreSlice {
base_dir: PathBuf,
content_dir: PathBuf,
meta: HashMap<ThingState, VecDeque<ThingMeta>>,
pub range_min: ThingId,
pub range_max: ThingId,
last_scrape: DateTime<Utc>,
}
impl ThingStoreSlice {
async fn new(base_dir: PathBuf, range_min: ThingId, range_max: ThingId) -> io::Result<Self> {
if range_max < range_min {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Programer Error: range_max ({range_max}) \
must be >= range_min ({range_min})"
),
));
}
let content_dir = base_dir.join("things");
ensure_dir_exists(&content_dir).await?;
let mut res = Self {
base_dir,
content_dir,
range_min,
range_max,
meta: ThingState::iter()
.map(|state| (state, VecDeque::new()))
.collect(),
last_scrape: earliest(),
};
if let Some(last_scrape) = read_last_scrape(last_scrape_file(&res.base_dir, false)).await? {
res.last_scrape = last_scrape;
}
res.read().await?;
Ok(res)
}
#[must_use]
pub fn num(&self, state: ThingState) -> ThingId {
ThingId::try_from(self.meta.get(&state).unwrap().len()).unwrap()
}
#[must_use]
pub fn next(&self, state: ThingState) -> Option<&ThingMeta> {
self.meta.get(&state).unwrap().front()
}
#[must_use]
pub fn next_id(&self, state: ThingState) -> Option<ThingId> {
self.next(state).map(|meta| meta.id)
}
#[must_use]
pub const fn size(&self) -> ThingId {
self.range_max - self.range_min + 1
}
pub async fn insert<S: AsRef<str>>(
&mut self,
thing_meta: ThingMeta,
thing: Option<S>,
thing_state_old: ThingState,
) -> io::Result<()> {
let state = thing_meta.state;
if matches!(state, ThingState::Untried) {
panic!("Programer Error: State {state} should never be inserted into the store");
}
if let Some(thing_val) = thing {
assert!(
state.has_content(),
"Programer Error: With state {:?}, \
we require no content of the thing, put it was provided",
thing_meta.state
);
self.write_thing_data(thing_meta.id, thing_val).await?;
} else if state.has_content() {
panic!(
"Programer Error: With state {:?}, \
we require the content of the thing, put it was not provided",
thing_meta.state
);
}
let thing_id = thing_meta.get_id();
let mut old_state_things = self.meta.get_mut(&thing_state_old).unwrap();
if let Some(next_thing_old_state) = old_state_things.front() {
if (next_thing_old_state.get_id() == thing_id) {
self.meta.get_mut(&ThingState::Untried).unwrap().pop_front();
} else {
return Err(io::Error::new(io::ErrorKind::NotFound,
format!("Failed to remove the thing with Id {thing_id} from the old state {thing_state_old}")));
}
}
self.meta.get_mut(&state).unwrap().push_back(thing_meta);
self.write(state).await?;
Ok(())
}
#[must_use]
pub fn cloned_os(&self) -> VecDeque<ThingId> {
self.meta
.get(&ThingState::OpenSource)
.unwrap()
.iter()
.map(|thing_meta| thing_meta.id)
.collect()
}
fn meta_file_path(&self, state: ThingState, temp: bool) -> PathBuf {
construct_file_path(&self.base_dir, format!("{state}.csv"), temp)
}
fn content_dir_path(&self) -> &Path {
self.content_dir.as_path()
}
#[must_use]
pub fn content_file_path(&self, thing_id: ThingId, temp: bool) -> PathBuf {
construct_file_path(self.content_dir_path(), format!("{thing_id}.json"), temp)
}
async fn write_thing_data<D: AsRef<str>>(
&self,
thing_id: ThingId,
thing_raw_api_response_content: D,
) -> io::Result<()> {
let temp_file_path = self.content_file_path(thing_id, true);
fs::write(&temp_file_path, thing_raw_api_response_content.as_ref()).await?;
fs::rename(temp_file_path, self.content_file_path(thing_id, false)).await?;
Ok(())
}
async fn write(&self, state: ThingState) -> io::Result<()> {
let temp_file_path = self.meta_file_path(state, true);
{
let mut things_meta_writer =
csv_async::AsyncSerializer::from_writer(fs::File::create(&temp_file_path).await?);
let values = self
.meta
.get(&state)
.expect("Programer Error: All ThingStates should always be in the map");
for thing_meta in values {
things_meta_writer.serialize(thing_meta).await?;
}
things_meta_writer.flush().await?;
}
fs::rename(temp_file_path, self.meta_file_path(state, false)).await?;
Ok(())
}
async fn read(&mut self) -> io::Result<()> {
let mut untried: BTreeSet<ThingId> = (self.range_min..=self.range_max).collect();
for state in ThingState::iter() {
let file_path = self.meta_file_path(state, false);
if (file_path.exists().await) {
let mut rdr =
csv_async::AsyncDeserializer::from_reader(fs::File::open(&file_path).await?);
let mut records = rdr.deserialize::<ThingMeta>();
while let Some(record) = records.next().await {
let mut thing_meta: ThingMeta = record?;
thing_meta.normalize();
untried.remove(&thing_meta.id);
self.meta
.get_mut(&thing_meta.state)
.unwrap()
.push_back(thing_meta);
}
}
}
let loaded_thing_meta_count = self
.meta
.values()
.map(|v| ThingId::try_from(v.len()).unwrap())
.sum::<ThingId>();
if loaded_thing_meta_count + ThingId::try_from(untried.len()).unwrap() != self.size() {
let msg = format!(
"Something is wrong with thing-slice {}-{} on disc: \
{} unique things in meta file, {} things (IDs) missing, \
which does not add up to the slices size: {}",
self.range_min,
self.range_max,
loaded_thing_meta_count,
untried.len(),
self.size()
);
return Err(io::Error::new(io::ErrorKind::InvalidData, msg));
}
let mut untried_queue = self.meta.get_mut(&ThingState::Untried).unwrap();
for thing_id in untried {
untried_queue.push_back(ThingMeta::new_untried(thing_id));
}
Ok(())
}
}
pub struct ThingStore {
root_dir: PathBuf,
range_min: ThingId,
range_max: ThingId,
slice_size: ThingId,
slices: HashMap<ThingId, Arc<sync::RwLock<ThingStoreSlice>>>,
last_scrape: DateTime<Utc>,
current_scrape_slice: ThingId,
next_scrape_slice: ThingId,
}
impl ThingStore {
pub async fn new(
root_dir: PathBuf,
range_min: ThingId,
range_max: ThingId,
) -> io::Result<Self> {
if range_max < range_min {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Programer Error: range_max ({range_max}) \
must be >= range_min ({range_min})"
),
));
}
let slice_size = DEFAULT_SLICE_SIZE;
if slice_size < MIN_SLICE_SIZE {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Programer Error: slice_size ({slice_size}) \
can not be smaller then ({MIN_SLICE_SIZE})"
),
));
}
ensure_dir_exists(&root_dir).await?;
let mut res = Self {
root_dir,
range_min,
range_max,
slice_size,
slices: HashMap::new(),
last_scrape: earliest(),
current_scrape_slice: range_min,
next_scrape_slice: range_min,
};
if let Some(last_scrape) = read_last_scrape(last_scrape_file(&res.root_dir, false)).await? {
res.last_scrape = last_scrape;
}
res.read_slice_being_scraped().await?;
Ok(res)
}
#[must_use]
pub const fn range(&self) -> ThingId {
self.range_max - self.range_min + 1
}
#[must_use]
pub const fn total_slices(&self) -> ThingId {
self.range() / self.slice_size
}
async fn create_new_slice(
&self,
slice_range_min: ThingId,
) -> io::Result<Arc<sync::RwLock<ThingStoreSlice>>> {
let slice_range_max = slice_range_min + self.slice_size;
let base_dir = self.root_dir.join("data").join(slice_range_min.to_string());
ensure_dir_exists(&base_dir).await?;
let slice = Arc::new(sync::RwLock::new(
ThingStoreSlice::new(base_dir, slice_range_min, slice_range_max).await?,
));
Ok(slice)
}
async fn get_slice(
&mut self,
slice_range_min: ThingId,
) -> io::Result<Arc<sync::RwLock<ThingStoreSlice>>> {
Ok(Arc::<_>::clone(
if let Some(child) = self.slices.get(&slice_range_min) {
child
} else {
let value = self.create_new_slice(slice_range_min).await?;
self.slices.insert(slice_range_min, value);
self.slices.get(&slice_range_min).unwrap()
},
))
}
pub async fn get_next_slice(&mut self) -> io::Result<Arc<sync::RwLock<ThingStoreSlice>>> {
let next = self.next_scrape_slice;
let slice = self.get_slice(self.next_scrape_slice).await?;
self.current_scrape_slice = self.next_scrape_slice;
self.next_scrape_slice = (self.next_scrape_slice + self.slice_size) % (self.range_max + 1);
Ok(slice)
}
#[must_use]
pub fn current_scrape_slice_file_path(&self) -> PathBuf {
construct_file_path(&self.root_dir, "last_scrape_slice.csv", false)
}
pub async fn write_slice_being_scraped(&self) -> io::Result<()> {
let file_path = self.current_scrape_slice_file_path();
fs::write(&file_path, self.current_scrape_slice.to_string()).await
}
pub async fn read_slice_being_scraped(&mut self) -> io::Result<()> {
let file_path = self.current_scrape_slice_file_path();
if file_path.as_path().exists().await {
let slice_min_str = fs::read_to_string(file_path).await?;
let slice_min: u32 = slice_min_str.parse::<ThingId>().map_err(|parse_err| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to parse last scraped slice ({slice_min_str}): {parse_err}"),
)
})?;
self.current_scrape_slice = slice_min;
self.next_scrape_slice = slice_min;
}
Ok(())
}
pub fn set_last_scrape(&mut self, time: DateTime<Utc>) {
self.last_scrape = time;
}
}