Init
This commit is contained in:
commit
ce49c3df81
29 changed files with 3466 additions and 0 deletions
3
.cargo/config
Normal file
3
.cargo/config
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[target.x86_64-unknown-linux-gnu]
|
||||||
|
linker = "clang"
|
||||||
|
rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold"]
|
9
.dockerignore
Normal file
9
.dockerignore
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/target
|
||||||
|
|
||||||
|
Dockerfile
|
||||||
|
.gitignore
|
||||||
|
.dockerignore
|
||||||
|
vendor/
|
||||||
|
examples/
|
||||||
|
# static/
|
||||||
|
LICENSE
|
2
.env
Normal file
2
.env
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
POSTGRES_USER="fiddle"
|
||||||
|
POSTGRES_PASSWORD="fiddle"
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
6
.sqlfluff
Normal file
6
.sqlfluff
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[sqlfluff]
|
||||||
|
dialect = postgres
|
||||||
|
exclude_rules = LT05
|
||||||
|
|
||||||
|
[sqlfluff:indentation]
|
||||||
|
tab_space_size = 4
|
2536
Cargo.lock
generated
Normal file
2536
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
30
Cargo.toml
Normal file
30
Cargo.toml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
[package]
|
||||||
|
name = "fiddle"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-cors = "0.6.4"
|
||||||
|
actix-web = "4.3.1"
|
||||||
|
anyhow = { version = "1.0.71", features = ["backtrace"] }
|
||||||
|
async-trait = "0.1.68"
|
||||||
|
chrono = "0.4.26"
|
||||||
|
dataloader = "0.16.0"
|
||||||
|
debug-ignore = "1.0.5"
|
||||||
|
diesel = { version = "2.1.0", features = [
|
||||||
|
"postgres",
|
||||||
|
"chrono",
|
||||||
|
"r2d2",
|
||||||
|
"uuid",
|
||||||
|
] }
|
||||||
|
env_logger = "0.10.0"
|
||||||
|
envconfig = "0.10.0"
|
||||||
|
juniper = { version = "0.15.11", features = ["uuid"] }
|
||||||
|
juniper_actix = "0.4.0"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
log = "0.4.18"
|
||||||
|
tokio = { version = "1.28.2", features = ["full"] }
|
||||||
|
uuid-simd = "0.8.0"
|
||||||
|
uuidv7 = { version = "1.3.2", package = "uuid", features = ["serde"] }
|
44
Dockerfile
Normal file
44
Dockerfile
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
FROM docker.io/lukemathwalker/cargo-chef:latest-rust-1.69.0 as chef
|
||||||
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
# hadolint ignore=DL3008,DL3009
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
wget \
|
||||||
|
software-properties-common \
|
||||||
|
clang
|
||||||
|
WORKDIR /tmp
|
||||||
|
ARG MOLD_VERSION="1.11.0"
|
||||||
|
RUN wget -qO- https://github.com/rui314/mold/releases/download/v${MOLD_VERSION}/mold-${MOLD_VERSION}-x86_64-linux.tar.gz | tar xzf - \
|
||||||
|
&& cp -RT ./mold-${MOLD_VERSION}-x86_64-linux /usr \
|
||||||
|
&& rm -rf ./mold-${MOLD_VERSION}-x86_64-linux
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
FROM chef as diesel
|
||||||
|
RUN cargo install diesel_cli --no-default-features --features postgres
|
||||||
|
|
||||||
|
FROM chef as planner
|
||||||
|
WORKDIR /usr/src/fiddle
|
||||||
|
RUN mkdir src && touch src/main.rs
|
||||||
|
COPY ./Cargo.toml ./Cargo.lock ./
|
||||||
|
RUN cargo chef prepare --recipe-path recipe.json
|
||||||
|
|
||||||
|
FROM chef as builder
|
||||||
|
WORKDIR /usr/src/fiddle
|
||||||
|
COPY ./.cargo ./.cargo
|
||||||
|
COPY --from=planner /usr/src/fiddle/recipe.json .
|
||||||
|
RUN cargo chef cook --release --recipe-path recipe.json
|
||||||
|
COPY ./assets ./assets
|
||||||
|
COPY ./src ./src
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
FROM docker.io/bitnami/minideb:bullseye as runner
|
||||||
|
RUN install_packages libpq5
|
||||||
|
WORKDIR /usr/local/bin
|
||||||
|
COPY --from=diesel /usr/local/cargo/bin/diesel .
|
||||||
|
WORKDIR /usr/src/fiddle
|
||||||
|
COPY ./run.sh .
|
||||||
|
RUN chmod +x ./run.sh
|
||||||
|
COPY ./migrations ./migrations
|
||||||
|
COPY --from=builder /usr/src/fiddle/target/release/fiddle ./bin/fiddle
|
||||||
|
EXPOSE 80
|
||||||
|
CMD [ "./run.sh" ]
|
6
assets/logo.txt
Normal file
6
assets/logo.txt
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
__ _ _ _ _
|
||||||
|
/ _(_) | | | | |
|
||||||
|
| |_ _ __| | __| | | ___
|
||||||
|
| _| |/ _` |/ _` | |/ _ \
|
||||||
|
| | | | (_| | (_| | | __/
|
||||||
|
|_| |_|\__,_|\__,_|_|\___|
|
9
diesel.toml
Normal file
9
diesel.toml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# For documentation on how to configure this file,
|
||||||
|
# see https://diesel.rs/guides/configuring-diesel-cli
|
||||||
|
|
||||||
|
[print_schema]
|
||||||
|
file = "src/schema.rs"
|
||||||
|
custom_type_derives = ["diesel::query_builder::QueryId"]
|
||||||
|
|
||||||
|
[migrations_directory]
|
||||||
|
dir = "migrations"
|
31
docker-compose.yml
Normal file
31
docker-compose.yml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: docker.io/postgres:15.2-alpine
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- db:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
fiddle:
|
||||||
|
image: git.dergrimm.net/dergrimm/fiddle:latest
|
||||||
|
build: .
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
FIDDLE_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER}
|
||||||
|
FIDDLE_DATA_DIR: /data
|
||||||
|
FIDDLE_AUTO_PRUNE_SLEEP: 1800
|
||||||
|
FIDDLE_TAMPER_SLEEP: 10
|
||||||
|
volumes:
|
||||||
|
- fiddle:/data
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db:
|
||||||
|
fiddle:
|
1
migrations/2023-05-31-130457_init/down.sql
Normal file
1
migrations/2023-05-31-130457_init/down.sql
Normal file
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE directories;
|
8
migrations/2023-05-31-130457_init/up.sql
Normal file
8
migrations/2023-05-31-130457_init/up.sql
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
|
||||||
|
CREATE TABLE directories (
|
||||||
|
id uuid PRIMARY KEY DEFAULT GEN_RANDOM_UUID(),
|
||||||
|
active boolean NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at timestamptz
|
||||||
|
);
|
10
run.sh
Normal file
10
run.sh
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DATABASE_URL="$FIDDLE_DB_URL" diesel setup \
|
||||||
|
--migration-dir ./migrations \
|
||||||
|
--locked-schema
|
||||||
|
|
||||||
|
RUST_LOG=info ./bin/fiddle
|
37
src/api/context.rs
Normal file
37
src/api/context.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
use dataloader::non_cached::Loader;
|
||||||
|
use juniper::FieldResult;
|
||||||
|
use juniper::IntoFieldError;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api::{loaders, Error},
|
||||||
|
db,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Loaders {
|
||||||
|
pub directory: loaders::directory::DirectoryLoader,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Loaders {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
directory: Loader::new(loaders::directory::DirectoryBatcher)
|
||||||
|
.with_yield_count(loaders::directory::YIELD_COUNT),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Context {
|
||||||
|
pub db_pool: db::Pool,
|
||||||
|
pub loaders: Loaders,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
pub fn get_db_conn(&self) -> FieldResult<db::PoolConnection> {
|
||||||
|
self.db_pool
|
||||||
|
.get()
|
||||||
|
.map_or(Err(Error::Internal.into_field_error()), Ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl juniper::Context for Context {}
|
58
src/api/error.rs
Normal file
58
src/api/error.rs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
use juniper::{graphql_value, FieldError, FieldResult, IntoFieldError, ScalarValue};
|
||||||
|
|
||||||
|
pub enum Error {
|
||||||
|
Internal,
|
||||||
|
DoesNotExist,
|
||||||
|
CountNegative,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: ScalarValue> IntoFieldError<S> for Error {
|
||||||
|
fn into_field_error(self) -> FieldError<S> {
|
||||||
|
match self {
|
||||||
|
Self::Internal => FieldError::new(
|
||||||
|
"Internal server error",
|
||||||
|
graphql_value!({
|
||||||
|
"type": "INTERNAL"
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Self::DoesNotExist => FieldError::new(
|
||||||
|
"Record does not exist",
|
||||||
|
graphql_value!({
|
||||||
|
"type": "DOES_NOT_EXIST"
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Self::CountNegative => FieldError::new(
|
||||||
|
"Count can not be negative",
|
||||||
|
graphql_value!({
|
||||||
|
"type": "COUNT_NEGATIVE",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait QueryResultIntoFieldResult<T> {
|
||||||
|
fn into_field_result(self) -> FieldResult<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> QueryResultIntoFieldResult<T> for diesel::QueryResult<T> {
|
||||||
|
fn into_field_result(self) -> FieldResult<T> {
|
||||||
|
self.map_err(|_| Error::Internal.into_field_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub trait AsyncResultIntoFieldResult<T> {
|
||||||
|
// fn into_field_result(self) -> FieldResult<T>;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl AsyncResultIntoFieldResult<celery::task::AsyncResult>
|
||||||
|
// for Result<celery::task::AsyncResult, celery::error::CeleryError>
|
||||||
|
// {
|
||||||
|
// fn into_field_result(self) -> FieldResult<celery::task::AsyncResult> {
|
||||||
|
// // match self {
|
||||||
|
// // Ok(x) => Ok(x),
|
||||||
|
// // Err(_) => Err(Error::Internal.into_field_error()),
|
||||||
|
// // }
|
||||||
|
// self.map_err(|_| Error::Internal.into_field_error())
|
||||||
|
// }
|
||||||
|
// }
|
43
src/api/loaders/directory.rs
Normal file
43
src/api/loaders/directory.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use dataloader::non_cached::Loader;
|
||||||
|
use dataloader::BatchFn;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use uuidv7::Uuid;
|
||||||
|
|
||||||
|
use crate::{api::models, db};
|
||||||
|
|
||||||
|
pub struct DirectoryBatcher;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl BatchFn<Uuid, models::directory::Directory> for DirectoryBatcher {
|
||||||
|
async fn load(&mut self, keys: &[Uuid]) -> HashMap<Uuid, models::directory::Directory> {
|
||||||
|
let db_conn = &mut db::POOL.get().unwrap();
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
for row in db::schema::directories::table
|
||||||
|
.select((
|
||||||
|
db::schema::directories::id,
|
||||||
|
db::schema::directories::created_at,
|
||||||
|
db::schema::directories::updated_at,
|
||||||
|
))
|
||||||
|
.filter(db::schema::directories::id.eq_any(keys))
|
||||||
|
.load::<(Uuid, DateTime<Utc>, Option<DateTime<Utc>>)>(db_conn)
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
let row: (Uuid, DateTime<Utc>, Option<DateTime<Utc>>) = row;
|
||||||
|
let data = models::directory::Directory {
|
||||||
|
id: row.0,
|
||||||
|
created_at: row.1,
|
||||||
|
updated_at: row.2,
|
||||||
|
};
|
||||||
|
map.insert(data.id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
map
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type DirectoryLoader = Loader<Uuid, models::directory::Directory, DirectoryBatcher>;
|
||||||
|
|
||||||
|
pub const YIELD_COUNT: usize = 100;
|
76
src/api/loaders/mod.rs
Normal file
76
src/api/loaders/mod.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::clone::Clone;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::hash::Hash;
|
||||||
|
use std::io::{Error, ErrorKind};
|
||||||
|
|
||||||
|
pub mod directory;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait TryOptionLoad<K, V>: Clone
|
||||||
|
where
|
||||||
|
K: Eq + Hash + Clone + Debug + Send + Sync,
|
||||||
|
V: Clone + Debug + Send,
|
||||||
|
{
|
||||||
|
async fn try_option_load(&self, key: K) -> Result<Option<V>, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<K, V, F> TryOptionLoad<K, V> for dataloader::non_cached::Loader<K, V, F>
|
||||||
|
where
|
||||||
|
K: Eq + Hash + Clone + Debug + Send + Sync,
|
||||||
|
V: Clone + Debug + Send,
|
||||||
|
F: dataloader::BatchFn<K, V> + Send + Sync,
|
||||||
|
{
|
||||||
|
async fn try_option_load(&self, key: K) -> Result<Option<V>, Error> {
|
||||||
|
async fn internal_try_option_load<K, V, F>(
|
||||||
|
loader: &dataloader::non_cached::Loader<K, V, F>,
|
||||||
|
key: K,
|
||||||
|
) -> Result<Option<V>, Error>
|
||||||
|
where
|
||||||
|
K: Eq + Hash + Clone + Debug + Send + Sync,
|
||||||
|
V: Clone + Debug + Send,
|
||||||
|
F: dataloader::BatchFn<K, V> + Send + Sync,
|
||||||
|
{
|
||||||
|
match loader.try_load(key).await {
|
||||||
|
Ok(x) => Ok(Some(x)),
|
||||||
|
Err(e) => match e.kind() {
|
||||||
|
ErrorKind::NotFound => Ok(None),
|
||||||
|
_ => Err(e),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal_try_option_load(self, key).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<K, V, F> TryOptionLoad<K, V> for dataloader::cached::Loader<K, V, F>
|
||||||
|
where
|
||||||
|
K: Eq + Hash + Clone + Debug + Send + Sync,
|
||||||
|
V: Clone + Debug + Send,
|
||||||
|
F: dataloader::BatchFn<K, V> + Send + Sync,
|
||||||
|
{
|
||||||
|
async fn try_option_load(&self, key: K) -> Result<Option<V>, Error> {
|
||||||
|
async fn internal_try_option_load<K, V, F>(
|
||||||
|
loader: &dataloader::cached::Loader<K, V, F>,
|
||||||
|
key: K,
|
||||||
|
) -> Result<Option<V>, Error>
|
||||||
|
where
|
||||||
|
K: Eq + Hash + Clone + Debug + Send + Sync,
|
||||||
|
V: Clone + Debug + Send,
|
||||||
|
F: dataloader::BatchFn<K, V> + Send + Sync,
|
||||||
|
{
|
||||||
|
match loader.try_load(key).await {
|
||||||
|
Ok(x) => Ok(Some(x)),
|
||||||
|
Err(e) => match e.kind() {
|
||||||
|
ErrorKind::NotFound => Ok(None),
|
||||||
|
_ => Err(e),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal_try_option_load(self, key).await
|
||||||
|
}
|
||||||
|
}
|
209
src/api/mod.rs
Normal file
209
src/api/mod.rs
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
use diesel::{prelude::*, row::Field};
|
||||||
|
use juniper::{graphql_object, EmptySubscription, FieldResult, IntoFieldError, RootNode};
|
||||||
|
use std::fs;
|
||||||
|
use uuidv7::Uuid;
|
||||||
|
|
||||||
|
use crate::{db, prune_many, prune_single, CONFIG};
|
||||||
|
|
||||||
|
pub mod context;
|
||||||
|
pub mod error;
|
||||||
|
pub mod loaders;
|
||||||
|
pub mod models;
|
||||||
|
pub mod scalars;
|
||||||
|
|
||||||
|
pub use context::Context;
|
||||||
|
pub use error::{Error, QueryResultIntoFieldResult};
|
||||||
|
|
||||||
|
use loaders::TryOptionLoad;
|
||||||
|
|
||||||
|
pub struct Query;
|
||||||
|
|
||||||
|
#[graphql_object(context = Context)]
|
||||||
|
impl Query {
|
||||||
|
fn ping() -> &'static str {
|
||||||
|
"pong"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn directories(context: &Context) -> FieldResult<Vec<models::directory::Directory>> {
|
||||||
|
let db_conn = &mut context.get_db_conn()?;
|
||||||
|
let ids: Vec<Uuid> = db::schema::directories::table
|
||||||
|
.select(db::schema::directories::id)
|
||||||
|
.filter(db::schema::directories::active)
|
||||||
|
.load(db_conn)
|
||||||
|
.into_field_result()?;
|
||||||
|
|
||||||
|
context
|
||||||
|
.loaders
|
||||||
|
.directory
|
||||||
|
.try_load_many(ids)
|
||||||
|
.await
|
||||||
|
.map_or_else(
|
||||||
|
|_| Err(Error::Internal.into_field_error()),
|
||||||
|
|x| Ok(x.into_values().collect()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn directory(
|
||||||
|
context: &Context,
|
||||||
|
id: scalars::Uuid,
|
||||||
|
) -> FieldResult<Option<models::directory::Directory>> {
|
||||||
|
context
|
||||||
|
.loaders
|
||||||
|
.directory
|
||||||
|
.try_option_load(*id)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::Internal.into_field_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Mutation;
|
||||||
|
|
||||||
|
#[graphql_object(context = Context)]
|
||||||
|
impl Mutation {
|
||||||
|
async fn create_directory(context: &Context) -> FieldResult<models::directory::Directory> {
|
||||||
|
let db_conn = &mut context.get_db_conn()?;
|
||||||
|
let id = diesel::insert_into(db::schema::directories::table)
|
||||||
|
.values(db::models::NewDirectory { active: true })
|
||||||
|
.returning(db::schema::directories::id)
|
||||||
|
.get_result::<Uuid>(db_conn)
|
||||||
|
.into_field_result()?;
|
||||||
|
|
||||||
|
fs::create_dir(format!("{}/{}", CONFIG.data_dir, id))
|
||||||
|
.map_err(|_| Error::Internal.into_field_error())?;
|
||||||
|
|
||||||
|
context
|
||||||
|
.loaders
|
||||||
|
.directory
|
||||||
|
.try_load(id)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::Internal.into_field_error())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_directories(
|
||||||
|
context: &Context,
|
||||||
|
count: i32,
|
||||||
|
) -> FieldResult<Vec<models::directory::Directory>> {
|
||||||
|
match count {
|
||||||
|
_ if count < 0 => return Err(Error::CountNegative.into_field_error()),
|
||||||
|
0 => return Ok(vec![]),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let db_conn = &mut context.get_db_conn()?;
|
||||||
|
|
||||||
|
let input = vec![db::models::NewDirectory { active: true }];
|
||||||
|
let ids = diesel::insert_into(db::schema::directories::table)
|
||||||
|
.values(
|
||||||
|
input
|
||||||
|
.iter()
|
||||||
|
.cycle()
|
||||||
|
.take(count as usize)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.returning(db::schema::directories::id)
|
||||||
|
.load::<Uuid>(db_conn)
|
||||||
|
.into_field_result()?;
|
||||||
|
|
||||||
|
for id in ids.iter() {
|
||||||
|
fs::create_dir(format!("{}/{}", CONFIG.data_dir, id))
|
||||||
|
.map_err(|_| Error::Internal.into_field_error())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
context
|
||||||
|
.loaders
|
||||||
|
.directory
|
||||||
|
.try_load_many(ids)
|
||||||
|
.await
|
||||||
|
.map_or_else(
|
||||||
|
|_| Err(Error::Internal.into_field_error()),
|
||||||
|
|x| Ok(x.into_values().collect()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_directory(
|
||||||
|
context: &Context,
|
||||||
|
id: scalars::Uuid,
|
||||||
|
immediate: Option<bool>,
|
||||||
|
) -> FieldResult<bool> {
|
||||||
|
let db_conn = &mut context.get_db_conn()?;
|
||||||
|
|
||||||
|
if diesel::select(diesel::dsl::not(diesel::dsl::exists(
|
||||||
|
db::schema::directories::table.filter(db::schema::directories::id.eq(*id)),
|
||||||
|
)))
|
||||||
|
.get_result::<bool>(db_conn)
|
||||||
|
.into_field_result()?
|
||||||
|
{
|
||||||
|
return Err(Error::DoesNotExist.into_field_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
if immediate.unwrap_or(false) {
|
||||||
|
prune_single(&id.to_string()).map_err(|_| Error::Internal.into_field_error())?;
|
||||||
|
diesel::delete(
|
||||||
|
db::schema::directories::table.filter(db::schema::directories::id.eq(*id)),
|
||||||
|
)
|
||||||
|
.execute(db_conn)
|
||||||
|
.into_field_result()?;
|
||||||
|
} else {
|
||||||
|
diesel::update(
|
||||||
|
db::schema::directories::table.filter(db::schema::directories::id.eq(*id)),
|
||||||
|
)
|
||||||
|
.set(db::schema::directories::active.eq(false))
|
||||||
|
.execute(db_conn)
|
||||||
|
.into_field_result()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_directories(
|
||||||
|
context: &Context,
|
||||||
|
ids: Vec<scalars::Uuid>,
|
||||||
|
immediate: Option<bool>,
|
||||||
|
) -> FieldResult<bool> {
|
||||||
|
let db_conn = &mut context.get_db_conn()?;
|
||||||
|
|
||||||
|
let ids: Vec<Uuid> = ids.into_iter().map(|id| *id).collect();
|
||||||
|
|
||||||
|
let count: i64 = db::schema::directories::table
|
||||||
|
.filter(db::schema::directories::id.eq_any(&ids))
|
||||||
|
.count()
|
||||||
|
.get_result::<i64>(db_conn)
|
||||||
|
.into_field_result()?;
|
||||||
|
dbg!(&count);
|
||||||
|
|
||||||
|
if count == ids.len() as i64 {
|
||||||
|
if immediate.unwrap_or(false) {
|
||||||
|
prune_many(&ids.iter().map(|id| id.to_string()).collect::<Vec<_>>())?;
|
||||||
|
diesel::delete(
|
||||||
|
db::schema::directories::table.filter(db::schema::directories::id.eq_any(ids)),
|
||||||
|
)
|
||||||
|
.execute(db_conn)
|
||||||
|
.into_field_result()?;
|
||||||
|
} else {
|
||||||
|
diesel::update(
|
||||||
|
db::schema::directories::table.filter(db::schema::directories::id.eq_any(ids)),
|
||||||
|
)
|
||||||
|
.set(db::schema::directories::active.eq(false))
|
||||||
|
.execute(db_conn)
|
||||||
|
.into_field_result()?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(Error::DoesNotExist.into_field_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn prune(context: &Context) -> FieldResult<bool> {
|
||||||
|
let db_conn = &mut context.get_db_conn()?;
|
||||||
|
crate::prune(db_conn).map_err(|_| Error::Internal.into_field_error())?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Schema = RootNode<'static, Query, Mutation, EmptySubscription<Context>>;
|
||||||
|
|
||||||
|
pub fn schema() -> Schema {
|
||||||
|
Schema::new(Query, Mutation, EmptySubscription::new())
|
||||||
|
}
|
31
src/api/models/directory.rs
Normal file
31
src/api/models/directory.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use juniper::graphql_object;
|
||||||
|
use uuidv7::Uuid;
|
||||||
|
|
||||||
|
use crate::api::{scalars, Context};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Directory {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_object(context = Context)]
|
||||||
|
impl Directory {
|
||||||
|
fn id(&self) -> scalars::Uuid {
|
||||||
|
scalars::Uuid(self.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self) -> String {
|
||||||
|
format!("{}", self.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn created_at(&self) -> DateTime<Utc> {
|
||||||
|
self.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
fn updated_at(&self) -> Option<DateTime<Utc>> {
|
||||||
|
self.updated_at
|
||||||
|
}
|
||||||
|
}
|
1
src/api/models/mod.rs
Normal file
1
src/api/models/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod directory;
|
3
src/api/scalars/mod.rs
Normal file
3
src/api/scalars/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod uuid;
|
||||||
|
|
||||||
|
pub use uuid::Uuid;
|
39
src/api/scalars/uuid.rs
Normal file
39
src/api/scalars/uuid.rs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
type Value = uuidv7::Uuid;
|
||||||
|
|
||||||
|
pub struct Uuid(pub Value);
|
||||||
|
|
||||||
|
#[juniper::graphql_scalar(name = "UUID", description = "UUID encoded as a string")]
|
||||||
|
impl<S> GraphQLScalar for Uuid
|
||||||
|
where
|
||||||
|
S: juniper::ScalarValue,
|
||||||
|
{
|
||||||
|
fn resolve(&self) -> juniper::Value {
|
||||||
|
juniper::Value::scalar(self.0.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_input_value(value: &juniper::InputValue) -> Option<Uuid> {
|
||||||
|
value
|
||||||
|
.as_string_value()
|
||||||
|
.and_then(|s| {
|
||||||
|
use uuid_simd::UuidExt;
|
||||||
|
use uuidv7::Uuid;
|
||||||
|
|
||||||
|
Uuid::parse(s.as_bytes()).ok()
|
||||||
|
})
|
||||||
|
.map(Uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_str<'a>(value: juniper::ScalarToken<'a>) -> juniper::ParseScalarResult<'a, S> {
|
||||||
|
<String as juniper::ParseScalarValue<S>>::from_str(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Uuid {
|
||||||
|
type Target = Value;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
22
src/config.rs
Normal file
22
src/config.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
use debug_ignore::DebugIgnore;
|
||||||
|
use envconfig::Envconfig;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
#[derive(Envconfig, Debug)]
|
||||||
|
pub struct Config {
|
||||||
|
#[envconfig(from = "FIDDLE_DB_URL")]
|
||||||
|
pub db_url: DebugIgnore<String>,
|
||||||
|
|
||||||
|
#[envconfig(from = "FIDDLE_DATA_DIR")]
|
||||||
|
pub data_dir: String,
|
||||||
|
|
||||||
|
#[envconfig(from = "FIDDLE_AUTO_PRUNE_SLEEP")]
|
||||||
|
pub auto_prune_sleep: u64,
|
||||||
|
|
||||||
|
#[envconfig(from = "FIDDLE_TAMPER_SLEEP")]
|
||||||
|
pub tamper_sleep: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref CONFIG: Config = Config::init_from_env().unwrap();
|
||||||
|
}
|
29
src/db/mod.rs
Normal file
29
src/db/mod.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use diesel::pg::PgConnection;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel::r2d2::{ConnectionManager, PooledConnection};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
use crate::CONFIG;
|
||||||
|
|
||||||
|
pub mod models;
|
||||||
|
pub mod schema;
|
||||||
|
|
||||||
|
pub type Pool = diesel::r2d2::Pool<ConnectionManager<PgConnection>>;
|
||||||
|
pub type Connection = PgConnection;
|
||||||
|
pub type PoolConnection = PooledConnection<ConnectionManager<PgConnection>>;
|
||||||
|
|
||||||
|
pub fn establish_connection() -> ConnectionResult<PgConnection> {
|
||||||
|
use diesel::Connection;
|
||||||
|
|
||||||
|
PgConnection::establish(&CONFIG.db_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pool() -> Result<Pool> {
|
||||||
|
Ok(diesel::r2d2::Pool::builder()
|
||||||
|
.build(ConnectionManager::<PgConnection>::new(&*CONFIG.db_url))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref POOL: Pool = pool().unwrap();
|
||||||
|
}
|
20
src/db/models.rs
Normal file
20
src/db/models.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use uuidv7::Uuid;
|
||||||
|
|
||||||
|
use crate::db::schema;
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable, Debug)]
|
||||||
|
#[diesel(table_name = schema::directories)]
|
||||||
|
pub struct Directory {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub active: bool,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = schema::directories)]
|
||||||
|
pub struct NewDirectory {
|
||||||
|
pub active: bool,
|
||||||
|
}
|
8
src/db/schema.rs
Normal file
8
src/db/schema.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
diesel::table! {
|
||||||
|
directories {
|
||||||
|
id -> Uuid,
|
||||||
|
active -> Bool,
|
||||||
|
created_at -> Timestamptz,
|
||||||
|
updated_at -> Nullable<Timestamptz>,
|
||||||
|
}
|
||||||
|
}
|
74
src/lib.rs
Normal file
74
src/lib.rs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use std::fs;
|
||||||
|
use uuidv7::Uuid;
|
||||||
|
|
||||||
|
pub mod api;
|
||||||
|
pub mod config;
|
||||||
|
pub mod db;
|
||||||
|
|
||||||
|
pub use config::CONFIG;
|
||||||
|
|
||||||
|
pub const ASCII_LOGO: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/logo.txt"));
|
||||||
|
|
||||||
|
pub fn init() {
|
||||||
|
println!("{}\n", ASCII_LOGO);
|
||||||
|
|
||||||
|
log::info!("Initializing config from environment variables");
|
||||||
|
let _ = *CONFIG;
|
||||||
|
log::info!("Config: {:?}", *CONFIG);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prune_single(entry: &str) -> Result<()> {
|
||||||
|
let path = format!("{}/{}", CONFIG.data_dir, entry);
|
||||||
|
log::info!("Pruning: {}", path);
|
||||||
|
|
||||||
|
if fs::metadata(&path).is_ok() {
|
||||||
|
fs::remove_file(&path).or_else(|_| fs::remove_dir_all(&path))?;
|
||||||
|
log::info!("File or directory deleted successfully: {}", path);
|
||||||
|
} else {
|
||||||
|
log::warn!("File or directory does not exist: {}", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prune_many(ids: &[String]) -> Result<()> {
|
||||||
|
ids.iter().try_for_each(|s| prune_single(s))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prune(db_conn: &mut db::Connection) -> Result<()> {
|
||||||
|
log::info!("Pruning deactivated directories");
|
||||||
|
|
||||||
|
let ids: Vec<Uuid> = db::schema::directories::table
|
||||||
|
.select(db::schema::directories::id)
|
||||||
|
.filter(db::schema::directories::active.eq(false))
|
||||||
|
.load(db_conn)?;
|
||||||
|
|
||||||
|
prune_many(&ids.iter().map(|id| id.to_string()).collect::<Vec<_>>())?;
|
||||||
|
diesel::delete(db::schema::directories::table.filter(db::schema::directories::id.eq_any(ids)))
|
||||||
|
.execute(db_conn)?;
|
||||||
|
|
||||||
|
log::info!("Pruning done");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prune_job() {
|
||||||
|
const NAME: &str = "prune";
|
||||||
|
|
||||||
|
log::info!("Starting cron job: {}", NAME);
|
||||||
|
|
||||||
|
let db_conn = &mut match db::POOL.get() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("{}: {:?}", NAME, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(e) = prune(db_conn) {
|
||||||
|
log::error!("{}: {:?}", NAME, e);
|
||||||
|
}
|
||||||
|
}
|
120
src/main.rs
Normal file
120
src/main.rs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
use actix_cors::Cors;
|
||||||
|
use actix_web::{http::header, middleware, web, App, HttpRequest, HttpResponse, HttpServer};
|
||||||
|
use anyhow::Result;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
use uuidv7::Uuid;
|
||||||
|
|
||||||
|
use juniper_actix::graphql_handler;
|
||||||
|
|
||||||
|
use fiddle::{api, db, init, prune_job, prune_many, CONFIG};
|
||||||
|
|
||||||
|
fn tamper_prune() -> Result<()> {
|
||||||
|
let db_conn = &mut db::POOL.get()?;
|
||||||
|
|
||||||
|
let entries = fs::read_dir(&CONFIG.data_dir)?;
|
||||||
|
let allowed: Vec<String> = db::schema::directories::table
|
||||||
|
.select(db::schema::directories::id)
|
||||||
|
.load::<Uuid>(db_conn)?
|
||||||
|
.iter()
|
||||||
|
.map(|id| id.to_string())
|
||||||
|
.collect();
|
||||||
|
let mut prunes: Vec<String> = vec![];
|
||||||
|
|
||||||
|
for p in entries {
|
||||||
|
let path = match p {
|
||||||
|
Ok(x) => x.path(),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("{:?}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let relative = match path.strip_prefix(&CONFIG.data_dir) {
|
||||||
|
Ok(x) => x.to_string_lossy().to_string(),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("{:?}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if path.is_file() || (path.is_dir() && !allowed.contains(&relative)) {
|
||||||
|
log::warn!("Invalid entry found: {}", relative);
|
||||||
|
prunes.push(relative);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prune_many(&prunes)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn not_found() -> &'static str {
|
||||||
|
"Not found!"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn graphql_route(
|
||||||
|
req: HttpRequest,
|
||||||
|
payload: web::Payload,
|
||||||
|
schema: web::Data<api::Schema>,
|
||||||
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
|
let context = api::Context {
|
||||||
|
db_pool: db::POOL.clone(),
|
||||||
|
loaders: api::context::Loaders::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
graphql_handler(&schema, &context, req, payload).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
let sleep_dur = Duration::from_secs(CONFIG.auto_prune_sleep);
|
||||||
|
loop {
|
||||||
|
prune_job();
|
||||||
|
thread::sleep(sleep_dur);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
let sleep_dur = Duration::from_secs(CONFIG.tamper_sleep);
|
||||||
|
loop {
|
||||||
|
if let Err(e) = tamper_prune() {
|
||||||
|
log::error!("{:?}", e);
|
||||||
|
}
|
||||||
|
thread::sleep(sleep_dur);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.app_data(web::Data::new(api::schema()))
|
||||||
|
.wrap(middleware::Logger::default())
|
||||||
|
.wrap(middleware::Compress::default())
|
||||||
|
.wrap(
|
||||||
|
Cors::default()
|
||||||
|
.allow_any_origin()
|
||||||
|
.allowed_methods(["POST", "GET"])
|
||||||
|
.allowed_headers([header::ACCEPT])
|
||||||
|
.allowed_header(header::CONTENT_TYPE)
|
||||||
|
.supports_credentials()
|
||||||
|
.max_age(3600),
|
||||||
|
)
|
||||||
|
.service(
|
||||||
|
web::resource("/graphql")
|
||||||
|
.route(web::post().to(graphql_route))
|
||||||
|
.route(web::get().to(graphql_route)),
|
||||||
|
)
|
||||||
|
.default_service(web::to(not_found))
|
||||||
|
})
|
||||||
|
.bind(("0.0.0.0", 8080))?
|
||||||
|
.run()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Reference in a new issue