Merge branch 'main' into change-test-db

This commit is contained in:
dullbananas 2023-12-20 16:16:15 -07:00 committed by GitHub
commit 23c64ea902
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 171 additions and 48 deletions

View file

@ -262,6 +262,19 @@ steps:
when: when:
event: cron event: cron
# using https://github.com/pksunkara/cargo-workspaces
publish_to_crates_io:
image: *rust_image
commands:
- 'echo "pub const VERSION: &str = \"$(git describe --tag)\";" > "crates/utils/src/version.rs"'
- cargo install cargo-workspaces
- cp -r migrations crates/db_schema/
- cargo login "$CARGO_API_TOKEN"
- cargo workspaces publish --from-git --allow-dirty --no-verify --allow-branch "${CI_COMMIT_TAG}" --yes custom "${CI_COMMIT_TAG}"
secrets: [cargo_api_token]
when:
event: tag
notify_on_failure: notify_on_failure:
image: alpine:3 image: alpine:3
commands: commands:

24
Cargo.lock generated
View file

@ -2523,7 +2523,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "lemmy_api" name = "lemmy_api"
version = "0.19.0" version = "0.19.1-rc.2"
dependencies = [ dependencies = [
"activitypub_federation", "activitypub_federation",
"actix-web", "actix-web",
@ -2551,7 +2551,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_api_common" name = "lemmy_api_common"
version = "0.19.0" version = "0.19.1-rc.2"
dependencies = [ dependencies = [
"activitypub_federation", "activitypub_federation",
"actix-web", "actix-web",
@ -2585,7 +2585,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_api_crud" name = "lemmy_api_crud"
version = "0.19.0" version = "0.19.1-rc.2"
dependencies = [ dependencies = [
"activitypub_federation", "activitypub_federation",
"actix-web", "actix-web",
@ -2607,7 +2607,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_apub" name = "lemmy_apub"
version = "0.19.0" version = "0.19.1-rc.2"
dependencies = [ dependencies = [
"activitypub_federation", "activitypub_federation",
"actix-web", "actix-web",
@ -2659,7 +2659,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_db_schema" name = "lemmy_db_schema"
version = "0.19.0" version = "0.19.1-rc.2"
dependencies = [ dependencies = [
"activitypub_federation", "activitypub_federation",
"anyhow", "anyhow",
@ -2696,7 +2696,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_db_views" name = "lemmy_db_views"
version = "0.19.0" version = "0.19.1-rc.2"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"chrono", "chrono",
@ -2715,7 +2715,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_db_views_actor" name = "lemmy_db_views_actor"
version = "0.19.0" version = "0.19.1-rc.2"
dependencies = [ dependencies = [
"chrono", "chrono",
"diesel", "diesel",
@ -2732,7 +2732,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_db_views_moderator" name = "lemmy_db_views_moderator"
version = "0.19.0" version = "0.19.1-rc.2"
dependencies = [ dependencies = [
"diesel", "diesel",
"diesel-async", "diesel-async",
@ -2744,7 +2744,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_federate" name = "lemmy_federate"
version = "0.19.0" version = "0.19.1-rc.2"
dependencies = [ dependencies = [
"activitypub_federation", "activitypub_federation",
"anyhow", "anyhow",
@ -2767,7 +2767,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_routes" name = "lemmy_routes"
version = "0.19.0" version = "0.19.1-rc.2"
dependencies = [ dependencies = [
"activitypub_federation", "activitypub_federation",
"actix-web", "actix-web",
@ -2791,7 +2791,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_server" name = "lemmy_server"
version = "0.19.0" version = "0.19.1-rc.2"
dependencies = [ dependencies = [
"activitypub_federation", "activitypub_federation",
"actix-cors", "actix-cors",
@ -2833,7 +2833,7 @@ dependencies = [
[[package]] [[package]]
name = "lemmy_utils" name = "lemmy_utils"
version = "0.19.0" version = "0.19.1-rc.2"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"anyhow", "anyhow",

View file

@ -1,5 +1,6 @@
[workspace.package] [workspace.package]
version = "0.19.0" version = "0.19.1-rc.2"
publish = false
edition = "2021" edition = "2021"
description = "A link aggregator for the fediverse" description = "A link aggregator for the fediverse"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -85,16 +86,16 @@ unused_self = "deny"
unwrap_used = "deny" unwrap_used = "deny"
[workspace.dependencies] [workspace.dependencies]
lemmy_api = { version = "=0.19.0", path = "./crates/api" } lemmy_api = { version = "=0.19.1-rc.2", path = "./crates/api" }
lemmy_api_crud = { version = "=0.19.0", path = "./crates/api_crud" } lemmy_api_crud = { version = "=0.19.1-rc.2", path = "./crates/api_crud" }
lemmy_apub = { version = "=0.19.0", path = "./crates/apub" } lemmy_apub = { version = "=0.19.1-rc.2", path = "./crates/apub" }
lemmy_utils = { version = "=0.19.0", path = "./crates/utils" } lemmy_utils = { version = "=0.19.1-rc.2", path = "./crates/utils" }
lemmy_db_schema = { version = "=0.19.0", path = "./crates/db_schema" } lemmy_db_schema = { version = "=0.19.1-rc.2", path = "./crates/db_schema" }
lemmy_api_common = { version = "=0.19.0", path = "./crates/api_common" } lemmy_api_common = { version = "=0.19.1-rc.2", path = "./crates/api_common" }
lemmy_routes = { version = "=0.19.0", path = "./crates/routes" } lemmy_routes = { version = "=0.19.1-rc.2", path = "./crates/routes" }
lemmy_db_views = { version = "=0.19.0", path = "./crates/db_views" } lemmy_db_views = { version = "=0.19.1-rc.2", path = "./crates/db_views" }
lemmy_db_views_actor = { version = "=0.19.0", path = "./crates/db_views_actor" } lemmy_db_views_actor = { version = "=0.19.1-rc.2", path = "./crates/db_views_actor" }
lemmy_db_views_moderator = { version = "=0.19.0", path = "./crates/db_views_moderator" } lemmy_db_views_moderator = { version = "=0.19.1-rc.2", path = "./crates/db_views_moderator" }
activitypub_federation = { version = "0.5.0-beta.6", default-features = false, features = [ activitypub_federation = { version = "0.5.0-beta.6", default-features = false, features = [
"actix-web", "actix-web",
] } ] }
@ -166,7 +167,7 @@ lemmy_utils = { workspace = true }
lemmy_db_schema = { workspace = true } lemmy_db_schema = { workspace = true }
lemmy_api_common = { workspace = true } lemmy_api_common = { workspace = true }
lemmy_routes = { workspace = true } lemmy_routes = { workspace = true }
lemmy_federate = { version = "0.19.0", path = "crates/federate" } lemmy_federate = { version = "0.19.1-rc.2", path = "crates/federate" }
activitypub_federation = { workspace = true } activitypub_federation = { workspace = true }
diesel = { workspace = true } diesel = { workspace = true }
diesel-async = { workspace = true } diesel-async = { workspace = true }

View file

@ -7,7 +7,7 @@
[![Translation status](http://weblate.join-lemmy.org/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.join-lemmy.org/engage/lemmy/) [![Translation status](http://weblate.join-lemmy.org/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.join-lemmy.org/engage/lemmy/)
[![License](https://img.shields.io/github/license/LemmyNet/lemmy.svg)](LICENSE) [![License](https://img.shields.io/github/license/LemmyNet/lemmy.svg)](LICENSE)
![GitHub stars](https://img.shields.io/github/stars/LemmyNet/lemmy?style=social) ![GitHub stars](https://img.shields.io/github/stars/LemmyNet/lemmy?style=social)
[![Delightful Humane Tech](https://codeberg.org/teaserbot-labs/delightful-humane-design/raw/branch/main/humane-tech-badge.svg)](https://codeberg.org/teaserbot-labs/delightful-humane-design) <a href="https://endsoftwarepatents.org/innovating-without-patents"><img style="height: 20px;" src="https://static.fsf.org/nosvn/esp/logos/patent-free.svg"></a>
</div> </div>

View file

@ -9,7 +9,7 @@ export RUST_LOG="warn,lemmy_server=debug,lemmy_federate=debug,lemmy_api=debug,le
export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queue has delays in the scale of 30s-5min export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queue has delays in the scale of 30s-5min
# pictrs setup # pictrs setup
if ! [ -f "pict-rs" ]; then if [ ! -f "pict-rs" ]; then
curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.0-beta.2/pict-rs-linux-amd64" -o api_tests/pict-rs curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.0-beta.2/pict-rs-linux-amd64" -o api_tests/pict-rs
chmod +x api_tests/pict-rs chmod +x api_tests/pict-rs
fi fi

View file

@ -10,6 +10,7 @@ import {
deletePrivateMessage, deletePrivateMessage,
unfollowRemotes, unfollowRemotes,
waitUntil, waitUntil,
reportPrivateMessage,
} from "./shared"; } from "./shared";
let recipient_id: number; let recipient_id: number;
@ -109,3 +110,42 @@ test("Delete a private message", async () => {
betaPms1.private_messages.length, betaPms1.private_messages.length,
); );
}); });
test("Create a private message report", async () => {
let pmRes = await createPrivateMessage(alpha, recipient_id);
let betaPms1 = await waitUntil(
() => listPrivateMessages(beta),
m =>
!!m.private_messages.find(
e =>
e.private_message.ap_id ===
pmRes.private_message_view.private_message.ap_id,
),
);
let betaPm = betaPms1.private_messages[0];
expect(betaPm).toBeDefined();
// Make sure that only the recipient can report it, so this should fail
await expect(
reportPrivateMessage(
alpha,
pmRes.private_message_view.private_message.id,
"a reason",
),
).rejects.toStrictEqual(Error("couldnt_create_report"));
// This one should pass
let reason = "another reason";
let report = await reportPrivateMessage(
beta,
betaPm.private_message.id,
reason,
);
expect(report.private_message_report_view.private_message.id).toBe(
betaPm.private_message.id,
);
expect(report.private_message_report_view.private_message_report.reason).toBe(
reason,
);
});

View file

@ -4,12 +4,14 @@ import {
BlockInstance, BlockInstance,
BlockInstanceResponse, BlockInstanceResponse,
CommunityId, CommunityId,
CreatePrivateMessageReport,
GetReplies, GetReplies,
GetRepliesResponse, GetRepliesResponse,
GetUnreadCountResponse, GetUnreadCountResponse,
InstanceId, InstanceId,
LemmyHttp, LemmyHttp,
PostView, PostView,
PrivateMessageReportResponse,
SuccessResponse, SuccessResponse,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { CreatePost } from "lemmy-js-client/dist/types/CreatePost"; import { CreatePost } from "lemmy-js-client/dist/types/CreatePost";
@ -781,6 +783,18 @@ export async function reportComment(
return api.createCommentReport(form); return api.createCommentReport(form);
} }
export async function reportPrivateMessage(
api: LemmyHttp,
private_message_id: number,
reason: string,
): Promise<PrivateMessageReportResponse> {
let form: CreatePrivateMessageReport = {
private_message_id,
reason,
};
return api.createPrivateMessageReport(form);
}
export async function listCommentReports( export async function listCommentReports(
api: LemmyHttp, api: LemmyHttp,
): Promise<ListCommentReportsResponse> { ): Promise<ListCommentReportsResponse> {

View file

@ -1,5 +1,6 @@
[package] [package]
name = "lemmy_api" name = "lemmy_api"
publish = false
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
description.workspace = true description.workspace = true

View file

@ -31,6 +31,11 @@ pub async fn create_pm_report(
let private_message_id = data.private_message_id; let private_message_id = data.private_message_id;
let private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; let private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?;
// Make sure that only the recipient of the private message can create a report
if person_id != private_message.recipient_id {
Err(LemmyErrorType::CouldntCreateReport)?
}
let report_form = PrivateMessageReportForm { let report_form = PrivateMessageReportForm {
creator_id: person_id, creator_id: person_id,
private_message_id, private_message_id,

View file

@ -98,9 +98,9 @@ impl ActivityChannel {
Ok(()) Ok(())
} }
pub async fn close(outgoing_activities_task: JoinHandle<LemmyResult<()>>) -> LemmyResult<()> { pub async fn close(outgoing_activities_task: JoinHandle<()>) -> LemmyResult<()> {
ACTIVITY_CHANNEL.keepalive_sender.lock().await.take(); ACTIVITY_CHANNEL.keepalive_sender.lock().await.take();
outgoing_activities_task.await??; outgoing_activities_task.await?;
Ok(()) Ok(())
} }
} }

View file

@ -1,5 +1,6 @@
[package] [package]
name = "lemmy_api_crud" name = "lemmy_api_crud"
publish = false
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
description.workspace = true description.workspace = true

View file

@ -1,5 +1,6 @@
[package] [package]
name = "lemmy_apub" name = "lemmy_apub"
publish = false
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
description.workspace = true description.workspace = true

View file

@ -225,11 +225,12 @@ where
Ok(()) Ok(())
} }
pub async fn handle_outgoing_activities(context: Data<LemmyContext>) -> LemmyResult<()> { pub async fn handle_outgoing_activities(context: Data<LemmyContext>) {
while let Some(data) = ActivityChannel::retrieve_activity().await { while let Some(data) = ActivityChannel::retrieve_activity().await {
match_outgoing_activities(data, &context.reset_request_count()).await? if let Err(e) = match_outgoing_activities(data, &context.reset_request_count()).await {
tracing::warn!("error while saving outgoing activity to db: {e}");
}
} }
Ok(())
} }
pub async fn match_outgoing_activities( pub async fn match_outgoing_activities(

View file

@ -87,7 +87,7 @@ impl Comment {
let updated_comment = diesel::update(comment.find(comment_id)) let updated_comment = diesel::update(comment.find(comment_id))
.set(path.eq(ltree)) .set(path.eq(ltree))
.get_result::<Self>(conn) .get_result::<Self>(conn)
.await; .await?;
// Update the child count for the parent comment_aggregates // Update the child count for the parent comment_aggregates
// You could do this with a trigger, but since you have to do this manually anyway, // You could do this with a trigger, but since you have to do this manually anyway,
@ -121,7 +121,7 @@ where ca.comment_id = c.id"
sql_query(update_child_count_stmt).execute(conn).await?; sql_query(update_child_count_stmt).execute(conn).await?;
} }
} }
updated_comment Ok(updated_comment)
}) as _ }) as _
}) })
.await .await

View file

@ -416,7 +416,8 @@ fn queries<'a>() -> Queries<
.unwrap_or(true) .unwrap_or(true)
{ {
// Do not hide read posts when it is a user profile view // Do not hide read posts when it is a user profile view
if let (Some(_creator_id), Some(person_id)) = (options.creator_id, my_person_id) { // Or, only hide read posts on non-profile views
if let (None, Some(person_id)) = (options.creator_id, my_person_id) {
query = query.filter(not(is_read(person_id))); query = query.filter(not(is_read(person_id)));
} }
} }
@ -752,7 +753,7 @@ mod tests {
local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm},
person::{Person, PersonInsertForm}, person::{Person, PersonInsertForm},
person_block::{PersonBlock, PersonBlockForm}, person_block::{PersonBlock, PersonBlockForm},
post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm}, post::{Post, PostInsertForm, PostLike, PostLikeForm, PostRead, PostUpdateForm},
}, },
traits::{Blockable, Crud, Joinable, Likeable}, traits::{Blockable, Crud, Joinable, Likeable},
utils::{build_db_pool_for_tests, DbPool, RANK_DEFAULT}, utils::{build_db_pool_for_tests, DbPool, RANK_DEFAULT},
@ -760,7 +761,7 @@ mod tests {
SubscribedType, SubscribedType,
}; };
use serial_test::serial; use serial_test::serial;
use std::time::Duration; use std::{collections::HashSet, time::Duration};
struct Data { struct Data {
inserted_instance: Instance, inserted_instance: Instance,
@ -1508,6 +1509,47 @@ mod tests {
cleanup(data, pool).await; cleanup(data, pool).await;
} }
#[tokio::test]
#[serial]
async fn post_listings_hide_read() {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let mut data = init_data(pool).await;
// Make sure local user hides read posts
let local_user_form = LocalUserUpdateForm {
show_read_posts: Some(false),
..Default::default()
};
let inserted_local_user =
LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form)
.await
.unwrap();
data.local_user_view.local_user = inserted_local_user;
// Mark a post as read
PostRead::mark_as_read(
pool,
HashSet::from([data.inserted_bot_post.id]),
data.local_user_view.person.id,
)
.await
.unwrap();
// Make sure you don't see the read post in the results
let post_listings_hide_read = PostQuery {
sort: Some(SortType::New),
local_user: Some(&data.local_user_view),
..Default::default()
}
.list(pool)
.await
.unwrap();
assert_eq!(1, post_listings_hide_read.len());
cleanup(data, pool).await;
}
async fn cleanup(data: Data, pool: &mut DbPool<'_>) { async fn cleanup(data: Data, pool: &mut DbPool<'_>) {
let num_deleted = Post::delete(pool, data.inserted_post.id).await.unwrap(); let num_deleted = Post::delete(pool, data.inserted_post.id).await.unwrap();
Community::delete(pool, data.inserted_community.id) Community::delete(pool, data.inserted_community.id)

View file

@ -210,7 +210,7 @@ mod tests {
.recipient_id(timmy.id) .recipient_id(timmy.id)
.content(message_content.clone()) .content(message_content.clone())
.build(); .build();
let _inserted_sara_timmy_message_form = PrivateMessage::create(pool, &sara_timmy_message_form) PrivateMessage::create(pool, &sara_timmy_message_form)
.await .await
.unwrap(); .unwrap();
@ -219,7 +219,7 @@ mod tests {
.recipient_id(jess.id) .recipient_id(jess.id)
.content(message_content.clone()) .content(message_content.clone())
.build(); .build();
let _inserted_sara_jess_message_form = PrivateMessage::create(pool, &sara_jess_message_form) PrivateMessage::create(pool, &sara_jess_message_form)
.await .await
.unwrap(); .unwrap();
@ -228,7 +228,7 @@ mod tests {
.recipient_id(sara.id) .recipient_id(sara.id)
.content(message_content.clone()) .content(message_content.clone())
.build(); .build();
let _inserted_timmy_sara_message_form = PrivateMessage::create(pool, &timmy_sara_message_form) PrivateMessage::create(pool, &timmy_sara_message_form)
.await .await
.unwrap(); .unwrap();
@ -237,13 +237,13 @@ mod tests {
.recipient_id(timmy.id) .recipient_id(timmy.id)
.content(message_content.clone()) .content(message_content.clone())
.build(); .build();
let _inserted_jess_timmy_message_form = PrivateMessage::create(pool, &jess_timmy_message_form) PrivateMessage::create(pool, &jess_timmy_message_form)
.await .await
.unwrap(); .unwrap();
let timmy_messages = PrivateMessageQuery { let timmy_messages = PrivateMessageQuery {
unread_only: false, unread_only: false,
creator_id: Option::None, creator_id: None,
..Default::default() ..Default::default()
} }
.list(pool, timmy.id) .list(pool, timmy.id)
@ -260,7 +260,7 @@ mod tests {
let timmy_unread_messages = PrivateMessageQuery { let timmy_unread_messages = PrivateMessageQuery {
unread_only: true, unread_only: true,
creator_id: Option::None, creator_id: None,
..Default::default() ..Default::default()
} }
.list(pool, timmy.id) .list(pool, timmy.id)
@ -320,7 +320,7 @@ mod tests {
let timmy_messages = PrivateMessageQuery { let timmy_messages = PrivateMessageQuery {
unread_only: true, unread_only: true,
creator_id: Option::None, creator_id: None,
..Default::default() ..Default::default()
} }
.list(pool, timmy.id) .list(pool, timmy.id)
@ -333,5 +333,8 @@ mod tests {
.await .await
.unwrap(); .unwrap();
assert_eq!(timmy_unread_messages, 1); assert_eq!(timmy_unread_messages, 1);
// This also deletes all persons and private messages thanks to sql `on delete cascade`
Instance::delete(pool, instance.id).await.unwrap();
} }
} }

View file

@ -230,7 +230,6 @@ mod tests {
#[tokio::test] #[tokio::test]
#[serial] #[serial]
#[allow(clippy::dbg_macro)]
async fn exclude_deleted() { async fn exclude_deleted() {
let pool = &build_db_pool_for_tests().await; let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into(); let pool = &mut pool.into();
@ -257,7 +256,6 @@ mod tests {
.list(pool) .list(pool)
.await .await
.unwrap(); .unwrap();
dbg!(&list);
assert_eq!(list.len(), 1); assert_eq!(list.len(), 1);
assert_eq!(list[0].person.id, data.bob.id); assert_eq!(list[0].person.id, data.bob.id);

View file

@ -1,5 +1,6 @@
[package] [package]
name = "lemmy_federate" name = "lemmy_federate"
publish = false
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
description.workspace = true description.workspace = true

View file

@ -171,6 +171,7 @@ impl InstanceWorker {
.await .await
.context("failed reading activity from db")? .context("failed reading activity from db")?
else { else {
tracing::debug!("{}: {:?} does not exist", self.instance.domain, id);
self.state.last_successful_id = Some(id); self.state.last_successful_id = Some(id);
continue; continue;
}; };

View file

@ -1,5 +1,6 @@
[package] [package]
name = "lemmy_routes" name = "lemmy_routes"
publish = false
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
description.workspace = true description.workspace = true

@ -1 +1 @@
Subproject commit 7a38baa7341cfa9299df5a80ebe42fe298380e40 Subproject commit 15815aea74fe97360afc03496b3ad62588649af0

View file

@ -77,7 +77,7 @@ services:
init: true init: true
pictrs: pictrs:
image: asonix/pictrs:0.4.0-beta.19 image: asonix/pictrs:0.5.0-rc.2
# this needs to match the pictrs url in lemmy.hjson # this needs to match the pictrs url in lemmy.hjson
hostname: pictrs hostname: pictrs
# we can set options to pictrs like this, here we set max. image size and forced format for conversion # we can set options to pictrs like this, here we set max. image size and forced format for conversion

View file

@ -49,7 +49,7 @@ services:
pictrs: pictrs:
restart: always restart: always
image: asonix/pictrs:0.4.0-beta.19 image: asonix/pictrs:0.5.0-rc.2
user: 991:991 user: 991:991
volumes: volumes:
- ./volumes/pictrs_alpha:/mnt:Z - ./volumes/pictrs_alpha:/mnt:Z