Skip to content

Commit dd77758

Browse files
committed
Close #713
1 parent e03cb02 commit dd77758

24 files changed

Lines changed: 1772 additions & 250 deletions

File tree

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ THOTH_EXPORT_API=http://localhost:8181
66
DATABASE_URL=postgres://thoth:thoth@localhost/thoth
77
# Full redis URL
88
REDIS_URL=redis://localhost:6379
9+
# AWS credentials for file uploads
10+
AWS_ACCESS_KEY_ID=
11+
AWS_SECRET_ACCESS_KEY=
12+
AWS_REGION=
913
# Logging level
1014
RUST_LOG=info
1115

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/bin/arguments/mod.rs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -163,15 +163,3 @@ pub fn aws_region() -> Arg {
163163
.help("AWS region for S3/CloudFront")
164164
.num_args(1)
165165
}
166-
167-
pub fn session() -> Arg {
168-
Arg::new("duration")
169-
.short('s')
170-
.long("session-length")
171-
.value_name("DURATION")
172-
.env("SESSION_DURATION_SECONDS")
173-
.default_value("3600")
174-
.help("Authentication cookie session duration (seconds)")
175-
.num_args(1)
176-
.value_parser(value_parser!(i64))
177-
}

src/bin/commands/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ lazy_static! {
2323
.arg(arguments::keep_alive("GRAPHQL_API_KEEP_ALIVE"))
2424
.arg(arguments::gql_url())
2525
.arg(arguments::key())
26-
.arg(arguments::zitadel_url());
26+
.arg(arguments::zitadel_url())
27+
.arg(arguments::aws_access_key_id())
28+
.arg(arguments::aws_secret_access_key())
29+
.arg(arguments::aws_region());
2730
}
2831

2932
lazy_static! {

src/bin/commands/start.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ lazy_static! {
1818
.arg(arguments::keep_alive("GRAPHQL_API_KEEP_ALIVE"))
1919
.arg(arguments::gql_url())
2020
.arg(arguments::key())
21-
.arg(arguments::zitadel_url()),
21+
.arg(arguments::zitadel_url())
22+
.arg(arguments::aws_access_key_id())
23+
.arg(arguments::aws_secret_access_key())
24+
.arg(arguments::aws_region()),
2225
)
2326
.subcommand(
2427
Command::new("export-api")
@@ -45,6 +48,7 @@ pub fn graphql_api(arguments: &ArgMatches) -> ThothResult<()> {
4548
.get_one::<String>("zitadel-url")
4649
.unwrap()
4750
.to_owned();
51+
4852
api_server(
4953
database_url,
5054
host,
@@ -54,6 +58,18 @@ pub fn graphql_api(arguments: &ArgMatches) -> ThothResult<()> {
5458
url,
5559
private_key,
5660
zitadel_url,
61+
arguments
62+
.get_one::<String>("aws-access-key-id")
63+
.unwrap()
64+
.to_owned(),
65+
arguments
66+
.get_one::<String>("aws-secret-access-key")
67+
.unwrap()
68+
.to_owned(),
69+
arguments
70+
.get_one::<String>("aws-region")
71+
.unwrap()
72+
.to_owned(),
5773
)
5874
.map_err(|e| e.into())
5975
}

thoth-api-server/src/lib.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use serde::Serialize;
1717
use thoth_api::{
1818
db::{init_pool, PgPool},
1919
graphql::{create_schema, Context, GraphQLRequest, Schema},
20+
storage::{create_cloudfront_client, create_s3_client, CloudFrontClient, S3Client},
2021
};
2122
use zitadel::{
2223
actix::introspection::{IntrospectedUser, IntrospectionConfigBuilder},
@@ -87,10 +88,17 @@ async fn graphql_schema(st: Data<Arc<Schema>>) -> HttpResponse {
8788
async fn graphql(
8889
st: Data<Arc<Schema>>,
8990
pool: Data<PgPool>,
91+
s3_client: Data<S3Client>,
92+
cloudfront_client: Data<CloudFrontClient>,
9093
user: Option<IntrospectedUser>,
9194
data: Json<GraphQLRequest>,
9295
) -> Result<HttpResponse, Error> {
93-
let ctx = Context::new(pool.into_inner(), user);
96+
let ctx = Context::new(
97+
pool.into_inner(),
98+
user,
99+
s3_client.into_inner(),
100+
cloudfront_client.into_inner(),
101+
);
94102
let result = data.execute(&st, &ctx).await;
95103
match result.is_ok() {
96104
true => Ok(HttpResponse::Ok().json(result)),
@@ -109,6 +117,9 @@ pub async fn start_server(
109117
public_url: String,
110118
private_key: String,
111119
zitadel_url: String,
120+
aws_access_key_id: String,
121+
aws_secret_access_key: String,
122+
aws_region: String,
112123
) -> io::Result<()> {
113124
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
114125

@@ -123,6 +134,10 @@ pub async fn start_server(
123134
.await
124135
.unwrap();
125136

137+
let s3_client = create_s3_client(&aws_access_key_id, &aws_secret_access_key, &aws_region).await;
138+
let cloudfront_client =
139+
create_cloudfront_client(&aws_access_key_id, &aws_secret_access_key, &aws_region).await;
140+
126141
HttpServer::new(move || {
127142
App::new()
128143
.wrap(Compress::default())
@@ -139,6 +154,8 @@ pub async fn start_server(
139154
.app_data(auth.clone())
140155
.app_data(Data::new(ApiConfig::new(public_url.clone())))
141156
.app_data(Data::new(init_pool(&database_url)))
157+
.app_data(Data::new(s3_client.clone()))
158+
.app_data(Data::new(cloudfront_client.clone()))
142159
.app_data(Data::new(Arc::new(create_schema())))
143160
.service(index)
144161
.service(graphql_index)

thoth-api/Cargo.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ backend = [
2828
"aws-config",
2929
"aws-credential-types",
3030
"base64",
31-
"hex",
32-
"aes-gcm"
31+
"hex"
3332
]
3433

3534
[dependencies]
@@ -63,7 +62,6 @@ aws-config = { version = "1", optional = true }
6362
aws-credential-types = { version = "1", optional = true }
6463
base64 = { version = "0.22", optional = true }
6564
hex = { version = "0.4", optional = true }
66-
aes-gcm = { version = "0.10", optional = true }
6765

6866
[dev-dependencies]
6967
fs2 = "0.4.3"
File renamed without changes.
File renamed without changes.

thoth-api/src/graphql/model.rs

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use crate::model::{
1717
contact::{Contact, ContactOrderBy, ContactType},
1818
contribution::{Contribution, ContributionType},
1919
contributor::Contributor,
20+
file::{File, FileType},
2021
funding::Funding,
2122
imprint::{Imprint, ImprintField, ImprintOrderBy},
2223
institution::{CountryCode, Institution},
@@ -40,18 +41,39 @@ use crate::model::{
4041
Crud, Doi, Isbn, Orcid, Ror, Timestamp,
4142
};
4243
use crate::policy::PolicyContext;
44+
use crate::storage::{CloudFrontClient, S3Client};
4345
use thoth_errors::ThothError;
4446

4547
impl juniper::Context for Context {}
4648

4749
pub struct Context {
4850
pub db: Arc<PgPool>,
4951
pub user: Option<IntrospectedUser>,
52+
pub s3_client: Arc<S3Client>,
53+
pub cloudfront_client: Arc<CloudFrontClient>,
5054
}
5155

5256
impl Context {
53-
pub fn new(pool: Arc<PgPool>, user: Option<IntrospectedUser>) -> Self {
54-
Self { db: pool, user }
57+
pub fn new(
58+
pool: Arc<PgPool>,
59+
user: Option<IntrospectedUser>,
60+
s3_client: Arc<S3Client>,
61+
cloudfront_client: Arc<CloudFrontClient>,
62+
) -> Self {
63+
Self {
64+
db: pool,
65+
user,
66+
s3_client,
67+
cloudfront_client,
68+
}
69+
}
70+
71+
pub fn s3_client(&self) -> &S3Client {
72+
self.s3_client.as_ref()
73+
}
74+
75+
pub fn cloudfront_client(&self) -> &CloudFrontClient {
76+
self.cloudfront_client.as_ref()
5577
}
5678
}
5779

@@ -667,6 +689,10 @@ impl Work {
667689
)
668690
.map_err(Into::into)
669691
}
692+
#[graphql(description = "Get the front cover file for this work")]
693+
pub fn frontcover(&self, context: &Context) -> FieldResult<Option<File>> {
694+
File::from_work_id(&context.db, &self.work_id).map_err(Into::into)
695+
}
670696
#[graphql(description = "Get references cited by this work")]
671697
pub fn references(
672698
&self,
@@ -907,12 +933,79 @@ impl Publication {
907933
.map_err(Into::into)
908934
}
909935

936+
#[graphql(description = "Get the publication file for this publication")]
937+
pub fn file(&self, context: &Context) -> FieldResult<Option<File>> {
938+
File::from_publication_id(&context.db, &self.publication_id).map_err(Into::into)
939+
}
940+
910941
#[graphql(description = "Get the work to which this publication belongs")]
911942
pub fn work(&self, context: &Context) -> FieldResult<Work> {
912943
Work::from_id(&context.db, &self.work_id).map_err(Into::into)
913944
}
914945
}
915946

947+
#[juniper::graphql_object(
948+
Context = Context,
949+
description = "A file stored in the system (publication file or front cover)."
950+
)]
951+
impl File {
952+
#[graphql(description = "Thoth ID of the file")]
953+
pub fn file_id(&self) -> &Uuid {
954+
&self.file_id
955+
}
956+
957+
#[graphql(description = "Type of file (publication or frontcover)")]
958+
pub fn file_type(&self) -> &FileType {
959+
&self.file_type
960+
}
961+
962+
#[graphql(description = "Thoth ID of the work (for frontcovers)")]
963+
pub fn work_id(&self) -> Option<&Uuid> {
964+
self.work_id.as_ref()
965+
}
966+
967+
#[graphql(description = "Thoth ID of the publication (for publication files)")]
968+
pub fn publication_id(&self) -> Option<&Uuid> {
969+
self.publication_id.as_ref()
970+
}
971+
972+
#[graphql(description = "S3 object key (canonical DOI-based path)")]
973+
pub fn object_key(&self) -> &String {
974+
&self.object_key
975+
}
976+
977+
#[graphql(description = "Public CDN URL")]
978+
pub fn cdn_url(&self) -> &String {
979+
&self.cdn_url
980+
}
981+
982+
#[graphql(description = "MIME type used when serving the file")]
983+
pub fn mime_type(&self) -> &String {
984+
&self.mime_type
985+
}
986+
987+
#[graphql(description = "Size of the file in bytes")]
988+
pub fn bytes(&self) -> i32 {
989+
// GraphQL does not support i64; files larger than 2GB will overflow.
990+
self.bytes as i32
991+
}
992+
993+
#[graphql(description = "SHA-256 checksum of the stored file")]
994+
pub fn sha256(&self) -> &String {
995+
&self.sha256
996+
}
997+
998+
#[graphql(description = "Date and time at which the file record was created")]
999+
pub fn created_at(&self) -> Timestamp {
1000+
self.created_at
1001+
}
1002+
1003+
#[graphql(description = "Date and time at which the file record was last updated")]
1004+
pub fn updated_at(&self) -> Timestamp {
1005+
self.updated_at
1006+
}
1007+
}
1008+
9161009
#[juniper::graphql_object(Context = Context, description = "An organisation that produces and distributes written texts.")]
9171010
impl Publisher {
9181011
#[graphql(description = "Thoth ID of the publisher")]

0 commit comments

Comments
 (0)