Merge pull request #329 from EdouardVanbelle/feat/drop-to-sytem
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
use serde::{Deserialize, Deserializer};
|
||||||
|
|
||||||
|
// deserialize comma separated string into into Vec<String>
|
||||||
|
pub fn deserialize_csv<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer).unwrap_or_default();
|
||||||
|
Ok(s.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(String::from)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
extract::{Json, State},
|
extract::{Json, Query, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
@@ -12,6 +12,7 @@ use crate::application::dtos::folder_dto::FolderDto;
|
|||||||
use crate::application::services::batch_operations::{
|
use crate::application::services::batch_operations::{
|
||||||
BatchOperationService, BatchResult, BatchStats,
|
BatchOperationService, BatchResult, BatchStats,
|
||||||
};
|
};
|
||||||
|
use crate::interfaces::api::deserializer;
|
||||||
use crate::interfaces::api::handlers::ApiResult;
|
use crate::interfaces::api::handlers::ApiResult;
|
||||||
use crate::interfaces::middleware::auth::AuthUser;
|
use crate::interfaces::middleware::auth::AuthUser;
|
||||||
|
|
||||||
@@ -646,6 +647,24 @@ pub struct BatchDownloadRequest {
|
|||||||
pub folder_ids: Vec<String>,
|
pub folder_ids: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct BatchDownloadQuery {
|
||||||
|
#[serde(default, deserialize_with = "deserializer::deserialize_csv")]
|
||||||
|
pub file_ids: Vec<String>, // will deserialize query string "1,2,3" into Vec<String>
|
||||||
|
#[serde(default, deserialize_with = "deserializer::deserialize_csv")]
|
||||||
|
pub folder_ids: Vec<String>, // will deserialize query string "1,2,3" into Vec<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert BatchDownloadQuery into BatchDownloadRequest
|
||||||
|
impl From<BatchDownloadQuery> for BatchDownloadRequest {
|
||||||
|
fn from(q: BatchDownloadQuery) -> Self {
|
||||||
|
Self {
|
||||||
|
file_ids: q.file_ids,
|
||||||
|
folder_ids: q.folder_ids,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Handler for moving multiple files and folders to trash in batch
|
/// Handler for moving multiple files and folders to trash in batch
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
@@ -832,6 +851,29 @@ pub async fn move_folders_batch(
|
|||||||
Ok((status_code, Json(response)).into_response())
|
Ok((status_code, Json(response)).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hander as a workarround for drag & drop (does not support POST requests)
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/batch/download",
|
||||||
|
params(
|
||||||
|
("file_ids" = Option<String>, Query, description = "Comma-separated file IDs"),
|
||||||
|
("folder_ids" = Option<String>, Query, description = "Comma-separated folder IDs"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "ZIP archive stream"),
|
||||||
|
(status = 400, description = "Bad request"),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 500, description = "ZIP creation failed")
|
||||||
|
),
|
||||||
|
tag = "batch"
|
||||||
|
)]
|
||||||
|
pub async fn download_batch_querystring(
|
||||||
|
State(state): State<BatchHandlerState>,
|
||||||
|
auth_user: AuthUser,
|
||||||
|
Query(params): Query<BatchDownloadQuery>,
|
||||||
|
) -> Result<Response, (StatusCode, String)> {
|
||||||
|
process_download_batch(state, auth_user, params.into()).await
|
||||||
|
}
|
||||||
/// Handler for downloading multiple files and folders as a single ZIP.
|
/// Handler for downloading multiple files and folders as a single ZIP.
|
||||||
///
|
///
|
||||||
/// The ZIP is written to a temporary file and streamed to the client,
|
/// The ZIP is written to a temporary file and streamed to the client,
|
||||||
@@ -847,10 +889,18 @@ pub async fn move_folders_batch(
|
|||||||
),
|
),
|
||||||
tag = "batch"
|
tag = "batch"
|
||||||
)]
|
)]
|
||||||
pub async fn download_batch(
|
pub async fn download_batch_post(
|
||||||
State(state): State<BatchHandlerState>,
|
State(state): State<BatchHandlerState>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Json(request): Json<BatchDownloadRequest>,
|
Json(request): Json<BatchDownloadRequest>,
|
||||||
|
) -> Result<Response, (StatusCode, String)> {
|
||||||
|
process_download_batch(state, auth_user, request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_download_batch(
|
||||||
|
state: BatchHandlerState,
|
||||||
|
auth_user: AuthUser,
|
||||||
|
request: BatchDownloadRequest,
|
||||||
) -> Result<Response, (StatusCode, String)> {
|
) -> Result<Response, (StatusCode, String)> {
|
||||||
if request.file_ids.is_empty() && request.folder_ids.is_empty() {
|
if request.file_ids.is_empty() && request.folder_ids.is_empty() {
|
||||||
return Err((
|
return Err((
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod cookie_auth;
|
pub mod cookie_auth;
|
||||||
|
pub mod deserializer;
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
|
|
||||||
@@ -130,7 +131,8 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
|
|||||||
handlers::batch_handler::get_folders_batch,
|
handlers::batch_handler::get_folders_batch,
|
||||||
handlers::batch_handler::move_folders_batch,
|
handlers::batch_handler::move_folders_batch,
|
||||||
handlers::batch_handler::trash_batch,
|
handlers::batch_handler::trash_batch,
|
||||||
handlers::batch_handler::download_batch,
|
handlers::batch_handler::download_batch_post,
|
||||||
|
handlers::batch_handler::download_batch_querystring,
|
||||||
// Music/playlist handlers (free functions)
|
// Music/playlist handlers (free functions)
|
||||||
handlers::music_handler::create_playlist,
|
handlers::music_handler::create_playlist,
|
||||||
handlers::music_handler::list_playlists,
|
handlers::music_handler::list_playlists,
|
||||||
|
|||||||
@@ -203,7 +203,9 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
|||||||
// Trash operations (soft delete)
|
// Trash operations (soft delete)
|
||||||
.route("/trash", post(batch_handler::trash_batch))
|
.route("/trash", post(batch_handler::trash_batch))
|
||||||
// Download as ZIP
|
// Download as ZIP
|
||||||
.route("/download", post(batch_handler::download_batch))
|
.route("/download", post(batch_handler::download_batch_post))
|
||||||
|
// work arround for drag & drop (does not support POST requests)
|
||||||
|
.route("/download", get(batch_handler::download_batch_querystring))
|
||||||
.with_state(batch_handler_state);
|
.with_state(batch_handler_state);
|
||||||
|
|
||||||
// Create search routes if the service is available
|
// Create search routes if the service is available
|
||||||
|
|||||||
@@ -1081,6 +1081,41 @@ const ui = {
|
|||||||
lastItemDiv = div;
|
lastItemDiv = div;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let downloadUrl;
|
||||||
|
let nameEncoded;
|
||||||
|
|
||||||
|
// tells Browser URL to call to drop selection on operating system (desktop, file manager etc)
|
||||||
|
// will generate a zipfile if multiple
|
||||||
|
if (selectedCardFromList.length === 1) {
|
||||||
|
// only 1 file
|
||||||
|
if (info.type === 'file') {
|
||||||
|
nameEncoded = info.name.replaceAll(/:/g, '-'); // issue is that DownloadURL is using : as separator;
|
||||||
|
downloadUrl = `${window.location.origin}/api/files/${info.id}`;
|
||||||
|
} else {
|
||||||
|
// directory into ZIP
|
||||||
|
nameEncoded = info.name.replaceAll(/:/g, '-').concat('.zip');
|
||||||
|
downloadUrl = `${window.location.origin}/api/folders/${info.id}/download?format=zip`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// must use ZIP container
|
||||||
|
// TODO better naming like ("selection in ${parent.name}") modulo i18n ? ...
|
||||||
|
const now = new Date().toISOString().replace(/T/, ' ').replace(/\.*/, '').replaceAll(/:/g, '-');
|
||||||
|
nameEncoded = `oxicloud ${now}.zip`;
|
||||||
|
const folders = [];
|
||||||
|
const files = [];
|
||||||
|
filesList.querySelectorAll(`div.selected`).forEach((e) => {
|
||||||
|
const item = itemInfo(e);
|
||||||
|
if (item.type === 'file') {
|
||||||
|
files.push(item.id);
|
||||||
|
} else {
|
||||||
|
folders.push(item.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
downloadUrl = `${window.location.origin}/api/batch/download?file_ids=${files.join(',')}&folder_ids=${folders.join(',')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.dataTransfer?.setData('DownloadURL', `application/octet-stream:${nameEncoded}:${downloadUrl}`);
|
||||||
|
|
||||||
// if more than 1 item, display the badge
|
// if more than 1 item, display the badge
|
||||||
if (selectedCardFromList.length > 1) {
|
if (selectedCardFromList.length > 1) {
|
||||||
const badge = document.createElement('span');
|
const badge = document.createElement('span');
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
// OxiCloud Service Worker
|
// OxiCloud Service Worker
|
||||||
// FIXME: generate cache name according build ?
|
// FIXME: generate cache name according build ?
|
||||||
const CACHE_NAME = 'oxicloud-cache-v20';
|
const CACHE_NAME = 'oxicloud-cache-v21';
|
||||||
|
|
||||||
// Only cache static assets — NOT HTML files.
|
// Only cache static assets — NOT HTML files.
|
||||||
// HTML files are served network-first so browsers always get the latest
|
// HTML files are served network-first so browsers always get the latest
|
||||||
|
|||||||
Reference in New Issue
Block a user