feat(drive): start implementation of drive

- add storage.drives
    - prepare migration phase
    - add created_by and updated_by on storage.folders
This commit is contained in:
Edouard Vanbelle
2026-06-18 13:29:41 +02:00
parent 77545aee05
commit eab7a609b9
43 changed files with 2434 additions and 154 deletions
@@ -0,0 +1,72 @@
//! `GET /api/drives` — list every drive the caller can read.
//!
//! D0 ships the read-only listing; D2 adds shared-drive membership
//! mutations (`POST/DELETE/PUT /api/drives/{id}/members`), D3 adds the
//! create-shared-drive flow, etc.
//!
//! The handler resolves the caller's expanded subject set through the
//! engine (so group-mediated drive grants surface — the foundation for
//! D2/D3) and asks the `DriveRepository` for every drive that set can
//! read. Authorization is purely the subject-expansion step: no
//! `require(...)` call here, because "your accessible drives" is a
//! listing query, not a permission decision on a specific drive.
use std::sync::Arc;
use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
use tracing::error;
use crate::application::dtos::drive_dto::DriveDto;
use crate::common::di::AppState;
use crate::domain::repositories::drive_repository::DriveRepository;
use crate::domain::services::authorization::Subject;
use crate::interfaces::errors::AppError;
use crate::interfaces::middleware::auth::AuthUser;
#[utoipa::path(
get,
path = "/api/drives",
responses(
(status = 200, description = "Drives the caller can read", body = Vec<DriveDto>),
(status = 500, description = "Internal server error"),
),
security(("bearerAuth" = [])),
tag = "drives"
)]
pub async fn list_drives(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
) -> impl IntoResponse {
let caller_id = auth_user.id;
// Expand the caller's `Subject::User` into the `(types, ids)` pair
// that includes every group the user transitively belongs to. The
// engine caches this expansion in its Moka cache; if the caller
// just ran a permission check, this is a hit.
let (subject_types, subject_ids) = match state
.authorization
.expand_subject_for_listing(Subject::User(caller_id))
.await
{
Ok(pair) => pair,
Err(e) => {
error!("list_drives: subject expansion failed: {e}");
return AppError::from(e).into_response();
}
};
match state
.drive_repo
.list_for_subjects(&subject_types, &subject_ids)
.await
{
Ok(drives) => {
let dtos: Vec<DriveDto> = drives.into_iter().map(DriveDto::from).collect();
(StatusCode::OK, Json(dtos)).into_response()
}
Err(e) => {
error!("list_drives: repo lookup failed: {e}");
AppError::internal_error(format!("Failed to list drives: {e}")).into_response()
}
}
}
@@ -719,6 +719,13 @@ pub async fn list_shared_with_me(
summary.resource_id
),
},
// Drive grants don't appear in the file/folder "Shared with me"
// listing — they're surfaced through `GET /api/drives` (D0).
// Silently skipping here is the right behaviour: a drive grant
// discovered by `list_incoming_resources_paged` is not a stale
// grant, just a different resource type with a different
// listing surface.
ResourceKind::Drive => continue,
}
}
@@ -953,6 +960,10 @@ pub async fn list_my_shares(
summary.resource_id
),
},
// Drive grants are surfaced via `GET /api/drives` (D0), not
// through the My Shares outgoing-resources surface. Silently
// skip — symmetric with the `list_shared_with_me` arm above.
ResourceKind::Drive => continue,
}
}
+1
View File
@@ -9,6 +9,7 @@ pub mod contacts_handler;
pub mod dedup_handler;
pub mod delta_upload_handler;
pub mod device_auth_handler;
pub mod drive_handler;
pub mod favorites_handler;
pub mod file_handler;
pub mod folder_handler;
+6
View File
@@ -13,6 +13,7 @@ use utoipa::{Modify, OpenApi};
use crate::application::dtos::contact_dto::{
AddressDto, ContactDto, ContactGroupDto, EmailDto, PhoneDto,
};
use crate::application::dtos::drive_dto::{DriveDto, DriveKindDto};
use crate::application::dtos::favorites_dto::{
BatchFavoritesResult, BatchFavoritesStats, FavoriteItemDto, FavoritesResourceItemDto,
};
@@ -165,6 +166,8 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
// Photos handler (free function)
handlers::photos_handler::list_photos,
handlers::photos_handler::list_photos_geo,
// Drive handler (free function)
handlers::drive_handler::list_drives,
// Batch handlers (free functions)
handlers::batch_handler::move_files_batch,
handlers::batch_handler::copy_files_batch,
@@ -359,6 +362,9 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
SharedWithMeDto,
SharedWithMeItemDto,
OutgoingResourceItemDto,
// Drive schemas
DriveDto,
DriveKindDto,
// Subject-group (ReBAC named groups) schemas
handlers::subject_group_handler::CreateGroupRequest,
handlers::subject_group_handler::UpdateGroupRequest,
+13
View File
@@ -440,6 +440,19 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
router = router.nest("/photos", photos_router);
}
// Drives — every drive the caller can read. D0 ships the read-only
// listing; D2 adds the membership API + shared-drive endpoints under
// `/api/drives/{id}/members`.
{
use crate::interfaces::api::handlers::drive_handler;
let drives_router = Router::new()
.route("/", get(drive_handler::list_drives))
.with_state(app_state.clone());
router = router.nest("/drives", drives_router);
}
// People (faces) routes — mounted only when OXICLOUD_ENABLE_FACES is on.
if app_state.people_service.is_some() {
use crate::interfaces::api::handlers::people_handler;