diff --git a/Cargo.lock b/Cargo.lock index fca6522..0d245bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -887,6 +887,7 @@ dependencies = [ "common", "sqlx", "testdir", + "time 0.3.27", "tokio", "tracing", "uuid", diff --git a/crates/accounts/src/api/routes/get_user_id_profile.rs b/crates/accounts/src/api/routes/get_user_id_profile.rs index 82c8d9e..6ff98bd 100644 --- a/crates/accounts/src/api/routes/get_user_id_profile.rs +++ b/crates/accounts/src/api/routes/get_user_id_profile.rs @@ -1,3 +1,20 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + use axum::http::StatusCode; pub(crate) async fn get_user_id_profile() -> std::result::Result { diff --git a/crates/common/src/config/configuration.rs b/crates/common/src/config/configuration.rs index 2008fe5..45d5dbb 100644 --- a/crates/common/src/config/configuration.rs +++ b/crates/common/src/config/configuration.rs @@ -16,7 +16,7 @@ */ //! This defines the app configuration -use std::{fmt, fs, path::PathBuf}; +use std::{fmt, fs}; use serde::Deserialize; use tracing::info; diff --git a/crates/common/src/database/details.rs b/crates/common/src/database/details.rs new file mode 100644 index 0000000..a7b713e --- /dev/null +++ b/crates/common/src/database/details.rs @@ -0,0 +1,45 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use uuid::Uuid; + +pub struct Details { + pub uuid: &'static Uuid, + pub camera_manufacturer: &'static str, + pub camera_model: &'static str, + pub camera_serial: &'static str, + pub lens_model: &'static str, + pub lens_serial: &'static str, + pub orientation: &'static str, + pub compression: &'static str, + pub resolution_x: &'static str, + pub resolution_y: &'static str, + pub resolution_unit: &'static str, + pub exposure_time: &'static str, + pub exposure_mode: &'static str, + pub exposure_program: &'static str, + pub exposure_bias: &'static str, + pub aperture: &'static f32, + pub iso: &'static i32, + pub color_space: &'static str, + pub pixel_x: &'static i64, + pub pixel_y: &'static i64, + pub user_comment: &'static str, + pub white_balance: &'static str, + pub flash: bool, + pub exif_version: &'static f32, +} diff --git a/crates/common/src/database/location.rs b/crates/common/src/database/location.rs new file mode 100644 index 0000000..bb928cc --- /dev/null +++ b/crates/common/src/database/location.rs @@ -0,0 +1,25 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use uuid::Uuid; + +pub struct Location { + pub uuid: &'static Uuid, + pub latitude: &'static f64, + pub longitude: &'static f64, + pub altitude: &'static Option, +} diff --git a/crates/common/src/database/media_item.rs b/crates/common/src/database/media_item.rs new file mode 100644 index 0000000..8c2db07 --- /dev/null +++ b/crates/common/src/database/media_item.rs @@ -0,0 +1,31 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use std::time::Instant; + +use super::{details::Details, location::Location, reference::Reference, tag::Tag}; + +pub struct MediaItem { + pub uuid: &'static str, + pub name: &'static str, + pub added_at: Instant, + pub taken_at: Option, + pub details: Option
, + pub tags: Option>, + pub location: Option, + pub references: Option>, +} diff --git a/crates/common/src/database/mod.rs b/crates/common/src/database/mod.rs index f09198b..a29110d 100644 --- a/crates/common/src/database/mod.rs +++ b/crates/common/src/database/mod.rs @@ -1,12 +1,85 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + use async_trait::async_trait; use std::error::Error; +use time::OffsetDateTime; use crate::auth::user::User; +use self::{media_item::MediaItem, reference::Reference}; + +pub mod details; +pub mod location; +pub mod media_item; +pub mod reference; +pub mod tag; + #[async_trait] pub trait Database { + /// Initialize the database and run required migrations async fn setup(&mut self) -> Result<(), Box>; + + /// List registered user accounts + async fn get_users(&self) -> Result, Box>; + + /// Create a new user account async fn create_user(&self, user: &User) -> Result<(), Box>; + + /// Get user by user_id + async fn get_user(&self, user_id: &str) -> Result>; + + /// Partial update a single user account async fn update_email(&self, email: &str, user_id: &str) -> Result<(), Box>; - async fn get_users(&self) -> Result, Box>; + async fn update_nickname(&self, nickname: &str) -> Result<(), Box>; + async fn update_names( + &self, + firstname: &str, + lastname: &str, + user_id: &str, + ) -> Result<(), Box>; + + async fn disable_user(&self, user_id: &str) -> Result<(), Box>; + async fn enable_user(&self, user_id: &str) -> Result<(), Box>; + + async fn get_media_items(&self, user_id: &str) -> Result, Box>; + async fn create_media_item( + &self, + user_id: &str, + name: &str, + date_taken: OffsetDateTime, + ) -> Result>; + async fn get_media_item(&self, media_id: &str) -> Result>; + async fn add_reference( + &self, + user_id: &str, + media_id: &str, + reference: &Reference, + ) -> Result>; + + async fn update_reference( + &self, + reference_id: &str, + reference: &Reference, + ) -> Result<(), Box>; + + async fn remove_reference( + &self, + media_id: &str, + reference_id: &str, + ) -> Result<(), Box>; } diff --git a/crates/common/src/database/reference.rs b/crates/common/src/database/reference.rs new file mode 100644 index 0000000..fb2e8f4 --- /dev/null +++ b/crates/common/src/database/reference.rs @@ -0,0 +1,28 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use time::OffsetDateTime; + +pub struct Reference { + pub uuid: &'static str, + pub filepath: String, + pub filename: String, + pub size: u64, + pub description: &'static str, + pub last_modified: OffsetDateTime, + pub is_missing: bool, +} diff --git a/crates/common/src/database/tag.rs b/crates/common/src/database/tag.rs new file mode 100644 index 0000000..6f9605b --- /dev/null +++ b/crates/common/src/database/tag.rs @@ -0,0 +1,24 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use uuid::Uuid; + +pub struct Tag { + pub uuid: &'static Uuid, + pub tag: &'static str, + pub origin: &'static str, +} diff --git a/crates/common/src/database/user.rs b/crates/common/src/database/user.rs new file mode 100644 index 0000000..8c2db07 --- /dev/null +++ b/crates/common/src/database/user.rs @@ -0,0 +1,31 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use std::time::Instant; + +use super::{details::Details, location::Location, reference::Reference, tag::Tag}; + +pub struct MediaItem { + pub uuid: &'static str, + pub name: &'static str, + pub added_at: Instant, + pub taken_at: Option, + pub details: Option
, + pub tags: Option>, + pub location: Option, + pub references: Option>, +} diff --git a/crates/database/Cargo.toml b/crates/database/Cargo.toml index 80ae5d9..522023d 100644 --- a/crates/database/Cargo.toml +++ b/crates/database/Cargo.toml @@ -25,3 +25,4 @@ sqlx = { workspace = true, features = ["runtime-tokio", "tls-native-tls", "postg [dev-dependencies] testdir.workspace = true +time.workspace = true diff --git a/crates/database/data/init.sql b/crates/database/data/init.sql deleted file mode 100644 index d00697b..0000000 --- a/crates/database/data/init.sql +++ /dev/null @@ -1,45 +0,0 @@ -CREATE TABLE IF NOT EXISTS users ( - --auto generated - uuid VARCHAR NOT NULL, - email VARCHAR, - password VARCHAR, - lastname VARCHAR, - firstname VARCHAR, - --indicates if user is not able to login - is_locked BOOLEAN DEFAULT FALSE, - create_at TIMESTAMPTZ DEFAULT NULL, - updated_at TIMESTAMPTZ DEFAULT NULL, - last_login TIMESTAMPTZ DEFAULT NULL, - PRIMARY KEY (uuid) -); - -CREATE TABLE IF NOT EXISTS media ( - uuid VARCHAR NOT NULL, - name VARCHAR, - owner VARCHAR NOT NULL, - -- show/hide by default regarding user settings - is_sensitive BOOLEAN DEFAULT FALSE, - -- added to photos.network - date_added TIMESTAMPTZ DEFAULT NULL, - -- captured timestamp - date_taken TIMESTAMPTZ DEFAULT NULL, - PRIMARY KEY (uuid), - FOREIGN KEY(owner) REFERENCES users (uuid) -); - -CREATE TABLE IF NOT EXISTS reference ( - uuid VARCHAR NOT NULL, - media VARCHAR NOT NULL, - -- ./data/files/[media.owner.uuid]/[media.date_taken.year]/[filename] - filepath VARCHAR NOT NULL, - -- original filename e.g. DSC1234.NEF - filename VARCHAR NOT NULL, - -- file size in bytes - size INTEGER NOT NULL, - -- xmp, metadata - last_modified TIMESTAMPTZ DEFAULT NULL, - -- reference got deleted from 3rd party - is_missing BOOLEAN DEFAULT TRUE, - PRIMARY KEY (uuid), - FOREIGN KEY(media) REFERENCES media (uuid) -); diff --git a/crates/database/migrations/0001_initial.sql b/crates/database/migrations/0001_initial.sql index 21177f5..e489500 100644 --- a/crates/database/migrations/0001_initial.sql +++ b/crates/database/migrations/0001_initial.sql @@ -5,31 +5,36 @@ CREATE TABLE IF NOT EXISTS users ( password VARCHAR, lastname VARCHAR, firstname VARCHAR, + displayname VARCHAR, --indicates if user is not able to login is_locked BOOLEAN DEFAULT FALSE, - create_at TIMESTAMPTZ DEFAULT NULL, + created_at TIMESTAMPTZ DEFAULT NULL, updated_at TIMESTAMPTZ DEFAULT NULL, - last_login TIMESTAMPTZ DEFAULT NULL, + last_login_at TIMESTAMPTZ DEFAULT NULL, PRIMARY KEY (uuid) ); CREATE TABLE IF NOT EXISTS media ( uuid VARCHAR NOT NULL, - name VARCHAR, + -- reference to `users` owner VARCHAR NOT NULL, + name VARCHAR, -- show/hide by default regarding user settings is_sensitive BOOLEAN DEFAULT FALSE, -- added to photos.network - date_added TIMESTAMPTZ DEFAULT NULL, + added_at TIMESTAMPTZ DEFAULT NULL, -- captured timestamp - date_taken TIMESTAMPTZ DEFAULT NULL, + taken_at TIMESTAMPTZ DEFAULT NULL, PRIMARY KEY (uuid), FOREIGN KEY(owner) REFERENCES users (uuid) ); CREATE TABLE IF NOT EXISTS reference ( uuid VARCHAR NOT NULL, + -- reference to `media` media VARCHAR NOT NULL, + -- reference to `users` + owner VARCHAR NOT NULL, -- ./data/files/[media.owner.uuid]/[media.date_taken.year]/[filename] filepath VARCHAR NOT NULL, -- original filename e.g. DSC1234.NEF @@ -39,8 +44,87 @@ CREATE TABLE IF NOT EXISTS reference ( description VARCHAR DEFAULT NULL, -- xmp, metadata last_modified TIMESTAMPTZ DEFAULT NULL, - -- reference got deleted from 3rd party - is_missing BOOLEAN DEFAULT TRUE, + PRIMARY KEY (uuid), + FOREIGN KEY(media) REFERENCES media (uuid) + FOREIGN KEY(owner) REFERENCES users (uuid) +); +CREATE TABLE IF NOT EXISTS details ( + uuid VARCHAR NOT NULL, + -- reference to `reference` + reference VARCHAR DEFAULT NULL, + -- NIKON + camera_manufacturer VARCHAR DEFAULT NULL, + -- Z7 + camera_model VARCHAR DEFAULT NULL, + -- 6014533 + camera_serial VARCHAR DEFAULT NULL, + -- NIKKOR Z 35mm f/1.8 S + lens_model VARCHAR DEFAULT NULL, + -- 20028476 + lens_serial VARCHAR DEFAULT NULL, + -- https://jdhao.github.io/2019/07/31/image_rotation_exif_info/ + orientation VARCHAR DEFAULT NULL, + -- JPEG compression + compression VARCHAR DEFAULT NULL, + -- 72.0 + resolution_x FLOAT DEFAULT NULL, + -- 72.0 + resolution_y FLOAT DEFAULT NULL, + -- Inch + resolution_unit VARCHAR DEFAULT NULL, + -- 1/400 s + exposure_time FLOAT DEFAULT NULL, + -- Auto exposure + exposure_mode VARCHAR DEFAULT NULL, + -- Aperture priority + exposure_program VARCHAR DEFAULT NULL, + -- 0 EV + exposure_bias VARCHAR DEFAULT NULL, + -- 1.8 + aperture FLOAT DEFAULT NULL, + focal_length VARCHAR DEFAULT NULL, + iso INTEGER NOT NULL, + -- sRGB + color_space VARCHAR DEFAULT NULL, + -- 8.256 pixel + pixel_x INTEGER NOT NULL, + -- 5.504 pixel + pixel_y INTEGER NOT NULL, + -- copyright info + user_comment VARCHAR DEFAULT NULL, + -- Auto white balance + white_balance VARCHAR DEFAULT NULL, + -- 0 = no flash + flash BOOL DEFAULT NULL, + -- Exif version 2.1 + exif_version FLOAT DEFAULT NULL, + FOREIGN KEY(reference) REFERENCES reference (uuid) +); + +CREATE TABLE IF NOT EXISTS tags ( + uuid VARCHAR NOT NULL, + -- language unaware string like "landscape" + tag VARCHAR NOT NULL, + -- reference to `media` + media VARCHAR NOT NULL, + -- plugin name or `USER` where the tag comes from + origin VARCHAR DEFAULT NULL, + PRIMARY KEY (uuid), + FOREIGN KEY(media) REFERENCES media (uuid) +); + +CREATE TABLE IF NOT EXISTS locations ( + uuid VARCHAR NOT NULL, + -- reference to `media` + media VARCHAR NOT NULL, + -- float gives a precision of ~1,7m + -- see https://stackoverflow.com/questions/159255/what-is-the-ideal-data-type-to-use-when-storing-latitude-longitude-in-a-mysql + -- 48.13750 + latitude FLOAT NOT NULL, + -- 11.57586 + longitude FLOAT NOT NULL, + -- 520 m + altitude FLOAT DEFAULT NULL, PRIMARY KEY (uuid), FOREIGN KEY(media) REFERENCES media (uuid) ); diff --git a/crates/database/src/postgres.rs b/crates/database/src/postgres.rs index 43f0055..befd2d8 100644 --- a/crates/database/src/postgres.rs +++ b/crates/database/src/postgres.rs @@ -19,6 +19,8 @@ //! use async_trait::async_trait; use common::auth::user::User; +use common::database::media_item::MediaItem; +use common::database::reference::Reference; use common::database::Database; use sqlx::types::time::OffsetDateTime; use sqlx::PgPool; @@ -34,7 +36,7 @@ pub struct PostgresDatabase { impl PostgresDatabase { pub async fn new(db_url: &str) -> Self { - let pool = sqlx::postgres::PgPool::connect(db_url).await.unwrap(); + let pool = PgPool::connect(db_url).await.unwrap(); PostgresDatabase { pool } } @@ -49,6 +51,31 @@ impl Database for PostgresDatabase { Ok(()) } + async fn get_users(&self) -> Result, Box> { + let query = "SELECT uuid, email, password, lastname, firstname FROM users"; + + let res = sqlx::query(query); + + let rows = res.fetch_all(&self.pool).await?; + + let users = rows + .iter() + .map(|row| User { + uuid: row.get("uuid"), + email: row.get("email"), + password: row.get("password"), + lastname: row.get("lastname"), + firstname: row.get("firstname"), + is_locked: false, + created_at: OffsetDateTime::now_utc(), + updated_at: None, + last_login: None, + }) + .collect(); + + Ok(users) + } + async fn create_user(&self, user: &User) -> Result<(), Box> { let query = "INSERT INTO users (uuid, email, password, lastname, firstname) VALUES ($1, $2, $3, $4, $5)"; let id = Uuid::new_v4().hyphenated().to_string(); @@ -65,6 +92,10 @@ impl Database for PostgresDatabase { Ok(()) } + async fn get_user(&self, _user_id: &str) -> Result> { + Err("Not implemented".into()) + } + async fn update_email(&self, email: &str, user_id: &str) -> Result<(), Box> { let query = "UPDATE users SET email = $1 WHERE uuid = $2"; @@ -77,28 +108,84 @@ impl Database for PostgresDatabase { Ok(()) } - async fn get_users(&self) -> Result, Box> { - let query = "SELECT uuid, email, password, lastname, firstname FROM users"; + async fn update_nickname(&self, _nickname: &str) -> Result<(), Box> { + Err("Not implemented".into()) + } - let res = sqlx::query(query); + async fn update_names( + &self, + _firstname: &str, + _lastname: &str, + _user_id: &str, + ) -> Result<(), Box> { + Err("Not implemented".into()) + } + + async fn disable_user(&self, _user_id: &str) -> Result<(), Box> { + Err("Not implemented".into()) + } + async fn enable_user(&self, _user_id: &str) -> Result<(), Box> { + Err("Not implemented".into()) + } + + async fn get_media_items(&self, _user_id: &str) -> Result, Box> { + Err("Not implemented".into()) + } + /// Creates a new media item if it doesn't exist and returns the media_id + async fn create_media_item( + &self, + user_id: &str, + name: &str, + date_taken: OffsetDateTime, + ) -> Result> { + let query = "SELECT COUNT(*) FROM media WHERE owner is $1 and taken_at like $2"; + let res = sqlx::query(query).bind(user_id).bind(date_taken); let rows = res.fetch_all(&self.pool).await?; - let users = rows - .iter() - .map(|row| User { - uuid: row.get("uuid"), - email: row.get("email"), - password: row.get("password"), - lastname: row.get("lastname"), - firstname: row.get("firstname"), - is_locked: false, - created_at: OffsetDateTime::now_utc(), - updated_at: None, - last_login: None, - }) - .collect(); + if rows.len() > 1 { + // TODO: return media item id for existing item + // rows.first() + } else { + // TODO: create a new media item and return id for new item + let query = "INSERT INTO media (uuid, name) VALUES ($1, $2)"; + let id = Uuid::new_v4().hyphenated().to_string(); + info!("create new media item with id `{}`.", id); + + sqlx::query(query) + .bind(id) + .bind(name) + .execute(&self.pool) + .await?; + } + + Ok("NOT IMPLEMENTED".to_string()) + } + async fn get_media_item(&self, _media_id: &str) -> Result> { + Err("Not implemented".into()) + } + async fn add_reference( + &self, + _user_id: &str, + _media_id: &str, + _reference: &Reference, + ) -> Result> { + Err("Not implemented".into()) + } - Ok(users) + async fn update_reference( + &self, + _reference_id: &str, + _reference: &Reference, + ) -> Result<(), Box> { + Err("Not implemented".into()) + } + + async fn remove_reference( + &self, + _media_id: &str, + _reference_id: &str, + ) -> Result<(), Box> { + Err("Not implemented".into()) } } diff --git a/crates/database/src/sqlite.rs b/crates/database/src/sqlite.rs index fbce878..be91beb 100644 --- a/crates/database/src/sqlite.rs +++ b/crates/database/src/sqlite.rs @@ -1,10 +1,32 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +//! This crate offers a database abstraction for [Photos.network](https://photos.network) core application. +//! use async_trait::async_trait; use common::auth::user::User; +use common::database::media_item::MediaItem; +use common::database::reference::Reference; use common::database::Database; use sqlx::types::time::OffsetDateTime; use sqlx::Row; use sqlx::SqlitePool; use std::error::Error; +use sqlx::sqlite::SqliteQueryResult; use tracing::info; use uuid::Uuid; @@ -24,11 +46,37 @@ impl SqliteDatabase { #[async_trait] impl Database for SqliteDatabase { async fn setup(&mut self) -> Result<(), Box> { + // run migrations from `migrations` directory sqlx::migrate!("./migrations").run(&self.pool).await?; Ok(()) } + async fn get_users(&self) -> Result, Box> { + let query = "SELECT uuid, email, password, lastname, firstname FROM users"; + + let res = sqlx::query(query); + + let rows = res.fetch_all(&self.pool).await?; + + let users = rows + .iter() + .map(|row| User { + uuid: row.get("uuid"), + email: row.get("email"), + password: row.get("password"), + lastname: row.get("lastname"), + firstname: row.get("firstname"), + is_locked: false, + created_at: OffsetDateTime::now_utc(), + updated_at: None, + last_login: None, + }) + .collect(); + + Ok(users) + } + async fn create_user(&self, user: &User) -> Result<(), Box> { let query = "INSERT INTO users (uuid, email, password, lastname, firstname) VALUES ($1, $2, $3, $4, $5)"; let id = Uuid::new_v4().hyphenated().to_string(); @@ -45,6 +93,10 @@ impl Database for SqliteDatabase { Ok(()) } + async fn get_user(&self, _user_id: &str) -> Result> { + Err("Not implemented".into()) + } + async fn update_email(&self, email: &str, user_id: &str) -> Result<(), Box> { let query = "UPDATE users SET email = $1 WHERE uuid = $2"; @@ -57,35 +109,98 @@ impl Database for SqliteDatabase { Ok(()) } - async fn get_users(&self) -> Result, Box> { - let query = "SELECT uuid, email, password, lastname, firstname FROM users"; + async fn update_nickname(&self, _nickname: &str) -> Result<(), Box> { + Err("Not implemented".into()) + } - let res = sqlx::query(query); + async fn update_names( + &self, + _firstname: &str, + _lastname: &str, + _user_id: &str, + ) -> Result<(), Box> { + Err("Not implemented".into()) + } - let rows = res.fetch_all(&self.pool).await?; + async fn disable_user(&self, _user_id: &str) -> Result<(), Box> { + Err("Not implemented".into()) + } + async fn enable_user(&self, _user_id: &str) -> Result<(), Box> { + Err("Not implemented".into()) + } - let users = rows - .iter() - .map(|row| User { - uuid: row.get("uuid"), - email: row.get("email"), - password: row.get("password"), - lastname: row.get("lastname"), - firstname: row.get("firstname"), - is_locked: false, - created_at: OffsetDateTime::now_utc(), - updated_at: None, - last_login: None, - }) - .collect(); + async fn get_media_items(&self, _user_id: &str) -> Result, Box> { + Err("Not implemented".into()) + } + async fn create_media_item( + &self, + user_id: &str, + name: &str, + date_taken: OffsetDateTime, + ) -> Result> { + // TODO: check if item with same `name` and `date_taken` already exists + let query = "INSERT INTO media (uuid, owner, name, is_sensitive, added_at, taken_at) VALUES ($1, $2, $3, $4, $5, $6)"; + let id = Uuid::new_v4().hyphenated().to_string(); + sqlx::query(query) + .bind(id.clone()) + .bind(&user_id) + .bind(&name) + .bind(false) + .bind(OffsetDateTime::now_utc()) + .bind(date_taken) + .execute(&self.pool) + .await?; - Ok(users) + Ok(id) + } + async fn get_media_item(&self, _media_id: &str) -> Result> { + Err("Not implemented".into()) + } + async fn add_reference( + &self, + user_id: &str, + media_id: &str, + reference: &Reference, + ) -> Result> { + let query = "INSERT INTO reference (uuid, media, owner, filepath, filename, size) VALUES ($1, $2, $3, $4, $5, $6)"; + let id = Uuid::new_v4().hyphenated().to_string(); + let res: SqliteQueryResult = sqlx::query(query) + .bind(id.clone()) + .bind(&media_id) + .bind(&user_id) + .bind(&reference.filepath) + .bind(&reference.filename) + .bind(&reference.size) + .execute(&self.pool) + .await?; + + Ok(id) + } + + async fn update_reference( + &self, + _reference_id: &str, + _reference: &Reference, + ) -> Result<(), Box> { + Err("Not implemented".into()) + } + + async fn remove_reference( + &self, + _media_id: &str, + _reference_id: &str, + ) -> Result<(), Box> { + Err("Not implemented".into()) } } #[allow(unused_imports)] mod tests { + use std::path::PathBuf; + use std::time::Instant; + use testdir::testdir; use super::*; + use time::format_description::well_known::Rfc3339; #[sqlx::test] async fn create_user_should_succeed(pool: SqlitePool) -> sqlx::Result<()> { @@ -98,7 +213,7 @@ mod tests { // when for i in 0..3 { let user = User { - uuid: uuid::Uuid::new_v4().hyphenated().to_string(), + uuid: Uuid::new_v4().hyphenated().to_string(), email: format!("test_{}@photos.network", i), password: Some("unsecure".into()), lastname: Some("Stuermer".into()), @@ -263,4 +378,87 @@ mod tests { Ok(()) } + + #[sqlx::test] + async fn create_media_item_should_succeed(pool: SqlitePool) -> sqlx::Result<()> { + // given + let user_id = "570DC079-664A-4496-BAA3-668C445A447"; + // create fake user - used as FOREIGN KEY in media + sqlx::query("INSERT INTO users (uuid, email, password, lastname, firstname) VALUES ($1, $2, $3, $4, $5)") + .bind(user_id.clone()) + .bind("info@photos.network") + .bind("unsecure") + .bind("Stuermer") + .bind("Benjamin") + .execute(&pool).await?; + let db = SqliteDatabase::new( + "target/sqlx/test-dbs/database/sqlite/tests/create_media_item_should_succeed.sqlite", + ).await; + + let name = "DSC_1234"; + let date_taken = OffsetDateTime::now_utc(); + + // when + let media_item_result = db.create_media_item(user_id.clone(), name, date_taken).await; + + // then + assert!(media_item_result.is_ok()); + + Ok(()) + } + + #[sqlx::test] + async fn add_reference_should_succeed(pool: SqlitePool) -> sqlx::Result<()> { + // given + let user_id = "570DC079-664A-4496-BAA3-668C445A447"; + let media_id = "ef9ac799-02f3-4b3f-9d96-7576be0434e6"; + let reference_id = "ef9ac799-02f3-4b3f-9d96-7576be0434e6"; + let added_at = OffsetDateTime::parse("2023-02-03T13:37:01.234567Z", &Rfc3339).unwrap(); + let taken_at = OffsetDateTime::parse("2023-01-01T13:37:01.234567Z", &Rfc3339).unwrap(); + // create fake user - used as FOREIGN KEY in reference + sqlx::query("INSERT INTO users (uuid, email, password, lastname, firstname) VALUES ($1, $2, $3, $4, $5)") + .bind(user_id.clone()) + .bind("info@photos.network") + .bind("unsecure") + .bind("Stuermer") + .bind("Benjamin") + .execute(&pool).await?; + // create fake media item - used as FOREIGN KEY in reference + sqlx::query("INSERT INTO media (uuid, owner, name, is_sensitive, added_at, taken_at) VALUES ($1, $2, $3, $4, $5, $6)") + .bind(media_id.clone()) + .bind(user_id.clone()) + .bind("DSC_1234") + .bind(false) + .bind(added_at) + .bind(taken_at) + .execute(&pool).await?; + let db = SqliteDatabase::new( + "target/sqlx/test-dbs/database/sqlite/tests/add_reference_should_succeed.sqlite", + ).await; + + let filename = "DSC_1234.jpg"; + let dir: PathBuf = testdir!(); + let path = dir.join(filename); + let filepath = path.clone().to_str().unwrap().to_string(); + std::fs::write(&path, "fake image data").ok(); + let metadata = std::fs::metadata(path.clone()).unwrap(); + + let reference = Reference { + uuid: reference_id, + filepath, + filename: filename.to_string(), + size: metadata.len(), + description: "", + last_modified: OffsetDateTime::parse("2023-02-03T13:37:01.234567Z", &Rfc3339).unwrap(), + is_missing: false, + }; + + // when + let add_reference_result = db.add_reference(user_id.clone(), media_id.clone(), &reference).await; + + // then + assert!(add_reference_result.is_ok()); + + Ok(()) + } } diff --git a/crates/media/src/api/router.rs b/crates/media/src/api/router.rs index d4cf2ed..5b3cdf0 100644 --- a/crates/media/src/api/router.rs +++ b/crates/media/src/api/router.rs @@ -41,7 +41,7 @@ impl MediaApi { where S: Send + Sync + Clone, { - let media_repository: MediaRepository = + let media_repository: MediaRepository = MediaRepository::new(state.database.clone(), state.config.clone()).await; let repository_state: MediaRepositoryState = Arc::new(media_repository); @@ -169,6 +169,45 @@ mod tests { assert_eq!(body, "list media items. limit=1000, offset=0"); } + + #[sqlx::test] + async fn post_media_without_user_fail(pool: SqlitePool) { + // given + let state: ApplicationState = ApplicationState { + config: Configuration::empty(), + plugins: HashMap::new(), + router: None, + database: SqliteDatabase { pool }, + }; + let app = Router::new().nest("/", MediaApi::routes(state).await); + + // when + let response = app + .oneshot( + Request::builder() + .uri("/media") + .method("POST") + .header(http::header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref()) + .header( + "Content-Disposition", + "attachment; filename=\"DSC_1234.NEF\"", + ) + //.body(Body::from(bytes)) + //.body(Body::empty()) + // TODO: add multipart file to body + .body(Body::from( + serde_json::to_vec(&json!([1, 2, 3, 4])).unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + // then + assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED); + } + + // TODO: test is failing due to missing multi-part body //#[sqlx::test] #[allow(dead_code)] diff --git a/crates/media/src/api/routes/delete_media_id.rs b/crates/media/src/api/routes/delete_media_id.rs index 34bcac2..f3a2654 100644 --- a/crates/media/src/api/routes/delete_media_id.rs +++ b/crates/media/src/api/routes/delete_media_id.rs @@ -1,3 +1,20 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + //! Deletes the given item owned by the user //! use axum::http::StatusCode; diff --git a/crates/media/src/api/routes/get_albums.rs b/crates/media/src/api/routes/get_albums.rs index 9e061a6..a415e99 100644 --- a/crates/media/src/api/routes/get_albums.rs +++ b/crates/media/src/api/routes/get_albums.rs @@ -1,3 +1,20 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + //! Returns the binary of a given entity //! diff --git a/crates/media/src/api/routes/get_albums_id.rs b/crates/media/src/api/routes/get_albums_id.rs index 8b49213..efdda53 100644 --- a/crates/media/src/api/routes/get_albums_id.rs +++ b/crates/media/src/api/routes/get_albums_id.rs @@ -1,3 +1,20 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + use axum::http::StatusCode; pub(crate) async fn get_albums_id() -> std::result::Result { diff --git a/crates/media/src/api/routes/get_media.rs b/crates/media/src/api/routes/get_media.rs index e18f42e..e225e78 100644 --- a/crates/media/src/api/routes/get_media.rs +++ b/crates/media/src/api/routes/get_media.rs @@ -1,3 +1,20 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + //! Returns a list of owned media items for current user //! use axum::extract::State; @@ -24,7 +41,7 @@ pub(crate) async fn get_media( Query(query): Query, ) -> Result, StatusCode> { let items: Result, DataAccessError> = - repo.get_media_items_for_user(Uuid::parse_str(user.uuid.as_str()).unwrap()); + repo.get_media_items_for_user(Uuid::parse_str(user.uuid.as_str()).unwrap()).await; match items { Ok(i) => { error!("Found {} items for user.", i.len()); diff --git a/crates/media/src/api/routes/get_media_id.rs b/crates/media/src/api/routes/get_media_id.rs index 2b659a8..4b618b9 100644 --- a/crates/media/src/api/routes/get_media_id.rs +++ b/crates/media/src/api/routes/get_media_id.rs @@ -1,3 +1,20 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + //! Returns a specific owned or shared media item for current user //! diff --git a/crates/media/src/api/routes/patch_albums_id.rs b/crates/media/src/api/routes/patch_albums_id.rs index d8fee22..796d652 100644 --- a/crates/media/src/api/routes/patch_albums_id.rs +++ b/crates/media/src/api/routes/patch_albums_id.rs @@ -1,3 +1,20 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + use axum::http::StatusCode; pub(crate) async fn patch_albums_id() -> std::result::Result { diff --git a/crates/media/src/api/routes/patch_albums_id_share.rs b/crates/media/src/api/routes/patch_albums_id_share.rs index f0924af..549c4f2 100644 --- a/crates/media/src/api/routes/patch_albums_id_share.rs +++ b/crates/media/src/api/routes/patch_albums_id_share.rs @@ -1,3 +1,20 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + use axum::http::StatusCode; pub(crate) async fn patch_albums_id_share() -> std::result::Result { diff --git a/crates/media/src/api/routes/patch_albums_id_unshare.rs b/crates/media/src/api/routes/patch_albums_id_unshare.rs index 012483e..6ab4fe2 100644 --- a/crates/media/src/api/routes/patch_albums_id_unshare.rs +++ b/crates/media/src/api/routes/patch_albums_id_unshare.rs @@ -1,3 +1,20 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + use axum::http::StatusCode; pub(crate) async fn patch_albums_id_unshare() -> std::result::Result { diff --git a/crates/media/src/api/routes/patch_media_id.rs b/crates/media/src/api/routes/patch_media_id.rs index 6c84bfc..da24821 100644 --- a/crates/media/src/api/routes/patch_media_id.rs +++ b/crates/media/src/api/routes/patch_media_id.rs @@ -1,3 +1,20 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + //! Updates fields from a specific media item for current user //! diff --git a/crates/media/src/api/routes/photo_details.rs b/crates/media/src/api/routes/photo_details.rs index aef6227..20deac1 100644 --- a/crates/media/src/api/routes/photo_details.rs +++ b/crates/media/src/api/routes/photo_details.rs @@ -1,7 +1,22 @@ -//! Returns the details of a given media item -//! -pub(crate) async fn photo_details( +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ -) -> std::result::Result { +//! Returns the details of a given media item +//! +pub(crate) async fn photo_details() -> std::result::Result { Err(StatusCode::NOT_IMPLEMENTED) } diff --git a/crates/media/src/api/routes/post_albums.rs b/crates/media/src/api/routes/post_albums.rs index 9b7cad6..93bd775 100644 --- a/crates/media/src/api/routes/post_albums.rs +++ b/crates/media/src/api/routes/post_albums.rs @@ -1,3 +1,20 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + //! Returns the binary of a given entity //! diff --git a/crates/media/src/api/routes/post_media.rs b/crates/media/src/api/routes/post_media.rs index e1adc30..4eff1cf 100644 --- a/crates/media/src/api/routes/post_media.rs +++ b/crates/media/src/api/routes/post_media.rs @@ -1,11 +1,30 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + //! Creates a new media item to aggregate related files for current user //! use axum::{ extract::{Multipart, State}, http::StatusCode, }; +use time::format_description::well_known::Rfc3339; +use time::OffsetDateTime; use common::auth::user::User; -use tracing::{debug, error}; +use tracing::debug; use uuid::Uuid; use crate::{data::error::DataAccessError, repository::MediaRepositoryState}; @@ -14,21 +33,15 @@ pub(crate) async fn post_media( State(repo): State, user: User, mut multipart: Multipart, -) -> std::result::Result { +) -> Result { let mut name = None; let mut date_taken = None; while let Some(field) = multipart.next_field().await.unwrap() { if let Some(field_name) = field.name() { match field_name { - "name" => { - name = Some(field.text().await.unwrap()); - // debug!("name={}", field.text().await.unwrap()); - } - "date_taken" => { - date_taken = Some(field.text().await.unwrap()); - // debug!("date_taken={}", field.text().await.unwrap()); - } + "name" => name = Some(field.text().await.unwrap()), + "date_taken" => date_taken = Some(field.text().await.unwrap()), _ => continue, } } @@ -38,18 +51,23 @@ pub(crate) async fn post_media( return Err(StatusCode::BAD_REQUEST); } + let date = OffsetDateTime::parse(date_taken.unwrap().as_str(), &Rfc3339); + if date.is_err() { + return Err(StatusCode::CREATED); + } + let result = repo.create_media_item_for_user( Uuid::parse_str(user.uuid.as_str()).unwrap(), name.clone().unwrap(), - date_taken.clone().unwrap(), - ); + date.unwrap(), + ).await; match result { Ok(uuid) => { debug!( "name={}, taken={} => id={}", name.unwrap(), - date_taken.unwrap(), + date.unwrap(), uuid.clone().hyphenated().to_string() ); @@ -68,3 +86,132 @@ pub(crate) async fn post_media( } } } + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use std::io; + + use axum::Router; + use common::{config::configuration::Configuration, ApplicationState}; + use database::sqlite::SqliteDatabase; + use hyper::{Body, Request}; + use mime::BOUNDARY; + use sqlx::SqlitePool; + use tokio::fs::File; + use tower::ServiceExt; + + use crate::api::router::MediaApi; + use std::io::Write; + use std::path::PathBuf; + use axum::http::header::CONTENT_TYPE; + use hyper::header::CONNECTION; + use testdir::testdir; + use tokio::io::AsyncReadExt; + + use super::*; + + #[sqlx::test] + async fn post_media_unauthorized_should_fail(pool: SqlitePool) { + // given + let state: ApplicationState = ApplicationState { + config: Configuration::empty(), + plugins: HashMap::new(), + router: None, + database: SqliteDatabase { pool }, + }; + + let app = Router::new().nest("/", MediaApi::routes(state).await); + + // when + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/media") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + // then + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + #[sqlx::test] + async fn post_media_authorized_without_name_field(pool: SqlitePool) { + // given + let state: ApplicationState = ApplicationState { + config: Configuration::empty(), + plugins: HashMap::new(), + router: None, + database: SqliteDatabase { pool }, + }; + let app = Router::new().nest("/", MediaApi::routes(state).await); + let data = media_item_form_data().await.unwrap(); + + // when + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/media") + .header("Authorization", "FakeAuth") + .header(CONNECTION, "Keep-Alive") + .header( + CONTENT_TYPE, + format!("multipart/form-data; boundary={}", BOUNDARY) + ) + // .header(CONTENT_TYPE, &*format!("multipart/form-data; boundary={}", BOUNDARY)) + .body(data.into()) + .unwrap(), + ) + .await + .unwrap(); + + // then + assert_eq!(response.status(), StatusCode::OK); + } + + async fn media_item_form_data() -> io::Result> { + let mut data: Vec = Vec::new(); + + write!(data, "--{}\r\n", BOUNDARY)?; + write!(data, "Content-Disposition: form-data; name=\"name\";\r\n")?; + write!(data, "\r\n")?; + write!(data, "DSC_1234")?; + write!(data, "\r\n")?; + + write!(data, "--{}\r\n", BOUNDARY)?; + write!(data, "Content-Disposition: form-data; name=\"date_taken\";\r\n")?; + write!(data, "\r\n")?; + write!(data, "1985-04-12T23:20:50.52Z")?; + write!(data, "\r\n")?; + + write!(data, "--{}--\r\n", BOUNDARY)?; + + Ok(data) + } + + #[allow(dead_code)] + async fn image_data() -> io::Result> { + let dir: PathBuf = testdir!(); + let path = dir.join("11.jpg"); + std::fs::write(&path, "fake image data").ok(); + + let mut data: Vec = Vec::new(); + write!(data, "--{}\r\n", BOUNDARY)?; + write!(data, "Content-Disposition: form-data; name=\"DSC_1234\"; filename=\"11.jpg\"\r\n")?; + write!(data, "Content-Type: image/jpeg\r\n")?; + write!(data, "\r\n")?; + + let mut f = File::open(path).await?; + f.read_to_end(&mut data).await?; + + write!(data, "\r\n")?; // The key thing you are missing + write!(data, "--{}--\r\n", BOUNDARY)?; + + Ok(data) + } +} diff --git a/crates/media/src/api/routes/post_media_id.rs b/crates/media/src/api/routes/post_media_id.rs index fa66d6b..13f5d3c 100644 --- a/crates/media/src/api/routes/post_media_id.rs +++ b/crates/media/src/api/routes/post_media_id.rs @@ -1,3 +1,20 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + //! Add files for a specific media item //! diff --git a/crates/media/src/repository.rs b/crates/media/src/repository.rs index cdeef35..f6040c8 100644 --- a/crates/media/src/repository.rs +++ b/crates/media/src/repository.rs @@ -17,18 +17,18 @@ use axum::async_trait; use common::config::configuration::Configuration; +use common::database::Database; +use database::sqlite::SqliteDatabase; use std::sync::Arc; -use std::time::Instant; use time::OffsetDateTime; use tracing::info; use uuid::Uuid; - use crate::data::error::DataAccessError; use crate::data::media_item::MediaItem; #[allow(dead_code)] -pub struct MediaRepository { - pub(crate) database: D, +pub struct MediaRepository { + pub(crate) database: SqliteDatabase, pub(crate) config: Configuration, } @@ -38,50 +38,67 @@ pub type MediaRepositoryState = Arc; #[cfg_attr(test, mockall::automock)] #[async_trait] pub trait MediaRepositoryTrait { - // Gets a list of media items from the DB filted by user_id - fn get_media_items_for_user(&self, user_id: Uuid) -> Result, DataAccessError>; + // Gets a list of media items from the DB filtered by user_id + async fn get_media_items_for_user(&self, user_id: Uuid) -> Result, DataAccessError>; /// Create a new media item for the given user - fn create_media_item_for_user( + async fn create_media_item_for_user( &self, user_id: Uuid, name: String, - date_taken: String, + date_taken: OffsetDateTime, ) -> Result; } -impl MediaRepository { - pub async fn new(database: D, config: Configuration) -> Self { +impl MediaRepository { + pub async fn new(database: SqliteDatabase, config: Configuration) -> Self { Self { database, config } } } #[async_trait] -impl MediaRepositoryTrait for MediaRepository { - fn get_media_items_for_user(&self, user_id: Uuid) -> Result, DataAccessError> { +impl MediaRepositoryTrait for MediaRepository { + async fn get_media_items_for_user(&self, user_id: Uuid) -> Result, DataAccessError> { info!("get items for user {}", user_id); - // TODO: read from database - // TODO: read from filesystem - Ok(vec![MediaItem { - uuid: "", - name: "", - date_added: Instant::now(), - date_taken: None, - details: None, - tags: None, - location: None, - references: None, - }]) + let items_result = &self.database.get_media_items(user_id.hyphenated().to_string().as_str()).await; + match items_result { + Ok(items) => return Ok( + items + .into_iter() + .map(|d| MediaItem { + // TODO: fill in missing info like references, details, tags + // TODO: check references on filesystem + uuid: d.uuid, + name: d.name, + date_added: d.added_at, + date_taken: d.taken_at, + details: None, + tags: None, + location: None, + references: None, + }) + .collect() + ), + Err(_) => return Err(DataAccessError::OtherError), + } } /// inside impl - fn create_media_item_for_user( + async fn create_media_item_for_user( &self, user_id: Uuid, name: String, - date_taken: String, + date_taken: OffsetDateTime, ) -> Result { + + // TODO: map result to + let _ = &self.database.create_media_item( + user_id.hyphenated().to_string().as_str(), + name.as_str(), + date_taken + ).await; + // Err(DataAccessError::AlreadyExist) Ok(Uuid::new_v4()) } @@ -116,7 +133,7 @@ mod tests { let repository = MediaRepository::new(db, Configuration::empty()).await; // when - let result = repository.get_media_items_for_user(Uuid::new_v4()); + let result = repository.get_media_items_for_user(Uuid::new_v4()).await; // then assert_eq!(result.is_ok(), true); diff --git a/crates/plugin_interface/src/lib.rs b/crates/plugin_interface/src/lib.rs index 470f070..27bdf73 100644 --- a/crates/plugin_interface/src/lib.rs +++ b/crates/plugin_interface/src/lib.rs @@ -1,3 +1,20 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + //! This is the plugin interface definition for Photos.network //! //! The Photos.network core will look for available plugins during start up and diff --git a/crates/plugin_interface/src/model/command.rs b/crates/plugin_interface/src/model/command.rs index 086d74d..dcb4760 100644 --- a/crates/plugin_interface/src/model/command.rs +++ b/crates/plugin_interface/src/model/command.rs @@ -1,3 +1,20 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + #[repr(C)] #[derive(Debug, Clone, PartialEq, StableAbi)] pub struct PluginCommand { diff --git a/crates/plugin_interface/src/model/response.rs b/crates/plugin_interface/src/model/response.rs index 72e9d49..f78b232 100644 --- a/crates/plugin_interface/src/model/response.rs +++ b/crates/plugin_interface/src/model/response.rs @@ -1,3 +1,20 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + #[repr(C)] #[derive(Debug, Clone, PartialEq, Eq, StableAbi)] pub struct PluginResponse { diff --git a/src/lib.rs b/src/lib.rs index 4cd718e..3a9a7af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,7 +75,7 @@ pub async fn start_server() -> Result<()> { tracing::subscriber::set_global_default( fmt::Subscriber::builder() // subscriber configuration - .with_max_level(tracing::Level::DEBUG) + .with_max_level(tracing::Level::TRACE) .with_target(false) .finish() // add additional writers @@ -105,9 +105,14 @@ pub async fn start_server() -> Result<()> { .write(true) .create_new(true) .open("data/core.sqlite3"); - let db = SqliteDatabase::new("data/core.sqlite3").await; - let _ = db.clone().setup().await; - let users = db.clone().get_users().await; + + let mut db = SqliteDatabase::new("data/core.sqlite3").await; + + { + let _ = db.setup().await; + } + + let users = db.get_users().await; if users.unwrap().is_empty() { info!("No user found, create a default admin user. Please check `data/credentials.txt` for details."); let default_user = "photo@photos.network"; diff --git a/tests/api/authentication/signin.rs b/tests/api/authentication/signin.rs index f02a11e..733866c 100644 --- a/tests/api/authentication/signin.rs +++ b/tests/api/authentication/signin.rs @@ -1,3 +1,20 @@ +/* Photos.network · A privacy first photo storage and sharing service for fediverse. + * Copyright (C) 2020 Photos network developers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + //! Test authentication behaviour like invalid user or password //! @@ -9,7 +26,7 @@ mod tests { fn it_works() { let a = 3; let b = 1 + 1; - + assert_eq!(a, b, "we are testing addition with {} and {}", a, b); }