Adding listMedia endpoint, to view all your local image uploads.

- Fixes #4445
This commit is contained in:
Dessalines 2024-03-05 13:00:10 -05:00
parent 6778279bb6
commit 3c51eaeb88
10 changed files with 109 additions and 26 deletions

View file

@ -27,7 +27,7 @@
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-prettier": "^5.0.1", "eslint-plugin-prettier": "^5.0.1",
"jest": "^29.5.0", "jest": "^29.5.0",
"lemmy-js-client": "0.19.4-alpha.6", "lemmy-js-client": "0.19.4-alpha.7",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",
"typescript": "^5.3.3" "typescript": "^5.3.3"

View file

@ -30,8 +30,8 @@ devDependencies:
specifier: ^29.5.0 specifier: ^29.5.0
version: 29.7.0(@types/node@20.11.22) version: 29.7.0(@types/node@20.11.22)
lemmy-js-client: lemmy-js-client:
specifier: 0.19.4-alpha.6 specifier: 0.19.4-alpha.7
version: 0.19.4-alpha.6 version: 0.19.4-alpha.7
prettier: prettier:
specifier: ^3.2.5 specifier: ^3.2.5
version: 3.2.5 version: 3.2.5
@ -2390,8 +2390,8 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true dev: true
/lemmy-js-client@0.19.4-alpha.6: /lemmy-js-client@0.19.4-alpha.7:
resolution: {integrity: sha512-x4htMlpoZ7hzrhrIk82aompVxbpu2ZDWtmWNGraM0+27nUCDf6gYxJH5nb5R/o39BQe5KSHq6zoBdliBwAY40w==} resolution: {integrity: sha512-1xvSDlhJmU3IzhT2+pvqPWKHo0P/aYTlpObL3hLy1RgaZLapvn3W7XC48cOydas+MAm2WBFsiFX9bi5X+5FWFA==}
dependencies: dependencies:
cross-fetch: 4.0.0 cross-fetch: 4.0.0
form-data: 4.0.0 form-data: 4.0.0

View file

@ -48,6 +48,15 @@ test("Upload image and delete it", async () => {
const content = downloadFileSync(upload.url); const content = downloadFileSync(upload.url);
expect(content.length).toBeGreaterThan(0); expect(content.length).toBeGreaterThan(0);
// Ensure that it comes back with the list_media endpoint
const listMediaRes = await alphaImage.listMedia({});
expect(listMediaRes.images.length).toBe(1);
// The deleteUrl is a combination of the endpoint, delete token, and alias
let firstImage = listMediaRes.images[0];
let deleteUrl = `${alphaUrl}/pictrs/image/delete/${firstImage.pictrs_delete_token}/${firstImage.pictrs_alias}`;
expect(deleteUrl).toBe(upload.delete_url);
// delete image // delete image
const delete_form: DeleteImage = { const delete_form: DeleteImage = {
token: upload.files![0].delete_token, token: upload.files![0].delete_token,

View file

@ -0,0 +1,28 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
context::LemmyContext,
person::{ListMedia, ListMediaResponse},
};
use lemmy_db_schema::source::images::LocalImage;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyError;
/// Lists comment reports for a community if an id is supplied
/// or returns all comment reports for communities a user moderates
#[tracing::instrument(skip(context))]
pub async fn list_media(
data: Query<ListMedia>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<ListMediaResponse>, LemmyError> {
let page = data.page;
let limit = data.limit;
let images = LocalImage::get_all_paged_by_local_user_id(
&mut context.pool(),
local_user_view.local_user.id,
page,
limit,
)
.await?;
Ok(Json(ListMediaResponse { images }))
}

View file

@ -10,6 +10,7 @@ pub mod generate_totp_secret;
pub mod get_captcha; pub mod get_captcha;
pub mod list_banned; pub mod list_banned;
pub mod list_logins; pub mod list_logins;
pub mod list_media;
pub mod login; pub mod login;
pub mod logout; pub mod logout;
pub mod notifications; pub mod notifications;

View file

@ -32,7 +32,7 @@ pub async fn purge_person(
// Read the local user to get their images, and delete them // Read the local user to get their images, and delete them
if let Ok(local_user) = LocalUserView::read_person(&mut context.pool(), data.person_id).await { if let Ok(local_user) = LocalUserView::read_person(&mut context.pool(), data.person_id).await {
let pictrs_uploads = let pictrs_uploads =
LocalImage::get_all_by_local_user_id(&mut context.pool(), &local_user.local_user.id).await?; LocalImage::get_all_by_local_user_id(&mut context.pool(), local_user.local_user.id).await?;
for upload in pictrs_uploads { for upload in pictrs_uploads {
delete_image_from_pictrs(&upload.pictrs_alias, &upload.pictrs_delete_token, &context) delete_image_from_pictrs(&upload.pictrs_alias, &upload.pictrs_delete_token, &context)

View file

@ -1,7 +1,7 @@
use crate::sensitive::Sensitive; use crate::sensitive::Sensitive;
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId}, newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId},
source::site::Site, source::{images::LocalImage, site::Site},
CommentSortType, CommentSortType,
ListingType, ListingType,
PostListingMode, PostListingMode,
@ -418,3 +418,20 @@ pub struct UpdateTotp {
pub struct UpdateTotpResponse { pub struct UpdateTotpResponse {
pub enabled: bool, pub enabled: bool,
} }
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Get your user's image / media uploads.
pub struct ListMedia {
pub page: Option<i64>,
pub limit: Option<i64>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct ListMediaResponse {
pub images: Vec<LocalImage>,
}

View file

@ -1,11 +1,8 @@
use crate::{ use crate::{
newtypes::{DbUrl, LocalUserId}, newtypes::{DbUrl, LocalUserId},
schema::{ schema::{local_image, remote_image},
local_image::dsl::{local_image, local_user_id, pictrs_alias},
remote_image::dsl::{link, remote_image},
},
source::images::{LocalImage, LocalImageForm, RemoteImage, RemoteImageForm}, source::images::{LocalImage, LocalImageForm, RemoteImage, RemoteImageForm},
utils::{get_conn, DbPool}, utils::{get_conn, limit_and_offset, DbPool},
}; };
use diesel::{ use diesel::{
dsl::exists, dsl::exists,
@ -15,7 +12,6 @@ use diesel::{
ExpressionMethods, ExpressionMethods,
NotFound, NotFound,
QueryDsl, QueryDsl,
Table,
}; };
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use url::Url; use url::Url;
@ -23,27 +19,47 @@ use url::Url;
impl LocalImage { impl LocalImage {
pub async fn create(pool: &mut DbPool<'_>, form: &LocalImageForm) -> Result<Self, Error> { pub async fn create(pool: &mut DbPool<'_>, form: &LocalImageForm) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
insert_into(local_image) insert_into(local_image::table)
.values(form) .values(form)
.get_result::<Self>(conn) .get_result::<Self>(conn)
.await .await
} }
/// This should only be used in the internal API, since it has no page and limit
pub async fn get_all_by_local_user_id( pub async fn get_all_by_local_user_id(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
user_id: &LocalUserId, user_id: LocalUserId,
) -> Result<Vec<Self>, Error> { ) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
local_image local_image::table
.filter(local_user_id.eq(user_id)) .filter(local_image::local_user_id.eq(user_id))
.select(local_image::all_columns()) .select(local_image::all_columns)
.load::<LocalImage>(conn)
.await
}
/// This is okay for API use.
pub async fn get_all_paged_by_local_user_id(
pool: &mut DbPool<'_>,
user_id: LocalUserId,
page: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?;
let (limit, offset) = limit_and_offset(page, limit)?;
local_image::table
.filter(local_image::local_user_id.eq(user_id))
.select(local_image::all_columns)
.limit(limit)
.offset(offset)
.load::<LocalImage>(conn) .load::<LocalImage>(conn)
.await .await
} }
pub async fn delete_by_alias(pool: &mut DbPool<'_>, alias: &str) -> Result<usize, Error> { pub async fn delete_by_alias(pool: &mut DbPool<'_>, alias: &str) -> Result<usize, Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
diesel::delete(local_image.filter(pictrs_alias.eq(alias))) diesel::delete(local_image::table.filter(local_image::pictrs_alias.eq(alias)))
.execute(conn) .execute(conn)
.await .await
} }
@ -56,7 +72,7 @@ impl RemoteImage {
.into_iter() .into_iter()
.map(|url| RemoteImageForm { link: url.into() }) .map(|url| RemoteImageForm { link: url.into() })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
insert_into(remote_image) insert_into(remote_image::table)
.values(forms) .values(forms)
.on_conflict_do_nothing() .on_conflict_do_nothing()
.execute(conn) .execute(conn)
@ -66,7 +82,9 @@ impl RemoteImage {
pub async fn validate(pool: &mut DbPool<'_>, link_: DbUrl) -> Result<(), Error> { pub async fn validate(pool: &mut DbPool<'_>, link_: DbUrl) -> Result<(), Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
let exists = select(exists(remote_image.filter((link).eq(link_)))) let exists = select(exists(
remote_image::table.filter(remote_image::link.eq(link_)),
))
.get_result::<bool>(conn) .get_result::<bool>(conn)
.await?; .await?;
if exists { if exists {

View file

@ -2,18 +2,26 @@ use crate::newtypes::{DbUrl, LocalUserId};
#[cfg(feature = "full")] #[cfg(feature = "full")]
use crate::schema::{local_image, remote_image}; use crate::schema::{local_image, remote_image};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
use std::fmt::Debug; use std::fmt::Debug;
use ts_rs::TS;
use typed_builder::TypedBuilder; use typed_builder::TypedBuilder;
#[skip_serializing_none] #[skip_serializing_none]
#[derive(PartialEq, Eq, Debug, Clone)] #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "full", derive(Queryable, Associations))] #[cfg_attr(
feature = "full",
derive(Queryable, Selectable, Identifiable, Associations, TS)
)]
#[cfg_attr(feature = "full", ts(export))]
#[cfg_attr(feature = "full", diesel(table_name = local_image))] #[cfg_attr(feature = "full", diesel(table_name = local_image))]
#[cfg_attr( #[cfg_attr(
feature = "full", feature = "full",
diesel(belongs_to(crate::source::local_user::LocalUser)) diesel(belongs_to(crate::source::local_user::LocalUser))
)] )]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", diesel(primary_key(local_user_id)))]
pub struct LocalImage { pub struct LocalImage {
pub local_user_id: LocalUserId, pub local_user_id: LocalUserId,
pub pictrs_alias: String, pub pictrs_alias: String,

View file

@ -29,6 +29,7 @@ use lemmy_api::{
get_captcha::get_captcha, get_captcha::get_captcha,
list_banned::list_banned_users, list_banned::list_banned_users,
list_logins::list_logins, list_logins::list_logins,
list_media::list_media,
login::login, login::login,
logout::logout, logout::logout,
notifications::{ notifications::{
@ -320,7 +321,8 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
.route("/totp/generate", web::post().to(generate_totp_secret)) .route("/totp/generate", web::post().to(generate_totp_secret))
.route("/totp/update", web::post().to(update_totp)) .route("/totp/update", web::post().to(update_totp))
.route("/list_logins", web::get().to(list_logins)) .route("/list_logins", web::get().to(list_logins))
.route("/validate_auth", web::get().to(validate_auth)), .route("/validate_auth", web::get().to(validate_auth))
.route("/list_media", web::get().to(list_media)),
) )
// Admin Actions // Admin Actions
.service( .service(