Skip to content

Commit e4ea221

Browse files
authored
Merge pull request #329 from EdouardVanbelle/feat/drop-to-sytem
2 parents 1cb541d + 0cb641a commit e4ea221

6 files changed

Lines changed: 108 additions & 5 deletions

File tree

src/interfaces/api/deserializer.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
use serde::{Deserialize, Deserializer};
2+
3+
// deserialize comma separated string into into Vec<String>
4+
pub fn deserialize_csv<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
5+
where
6+
D: Deserializer<'de>,
7+
{
8+
let s = String::deserialize(deserializer).unwrap_or_default();
9+
Ok(s.split(',')
10+
.map(str::trim)
11+
.filter(|s| !s.is_empty())
12+
.map(String::from)
13+
.collect())
14+
}

src/interfaces/api/handlers/batch_handler.rs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use axum::{
2-
extract::{Json, State},
2+
extract::{Json, Query, State},
33
http::StatusCode,
44
response::{IntoResponse, Response},
55
};
@@ -12,6 +12,7 @@ use crate::application::dtos::folder_dto::FolderDto;
1212
use crate::application::services::batch_operations::{
1313
BatchOperationService, BatchResult, BatchStats,
1414
};
15+
use crate::interfaces::api::deserializer;
1516
use crate::interfaces::api::handlers::ApiResult;
1617
use crate::interfaces::middleware::auth::AuthUser;
1718

@@ -646,6 +647,24 @@ pub struct BatchDownloadRequest {
646647
pub folder_ids: Vec<String>,
647648
}
648649

650+
#[derive(Debug, Deserialize)]
651+
pub struct BatchDownloadQuery {
652+
#[serde(default, deserialize_with = "deserializer::deserialize_csv")]
653+
pub file_ids: Vec<String>, // will deserialize query string "1,2,3" into Vec<String>
654+
#[serde(default, deserialize_with = "deserializer::deserialize_csv")]
655+
pub folder_ids: Vec<String>, // will deserialize query string "1,2,3" into Vec<String>
656+
}
657+
658+
// convert BatchDownloadQuery into BatchDownloadRequest
659+
impl From<BatchDownloadQuery> for BatchDownloadRequest {
660+
fn from(q: BatchDownloadQuery) -> Self {
661+
Self {
662+
file_ids: q.file_ids,
663+
folder_ids: q.folder_ids,
664+
}
665+
}
666+
}
667+
649668
/// Handler for moving multiple files and folders to trash in batch
650669
#[utoipa::path(
651670
post,
@@ -832,6 +851,29 @@ pub async fn move_folders_batch(
832851
Ok((status_code, Json(response)).into_response())
833852
}
834853

854+
// Hander as a workarround for drag & drop (does not support POST requests)
855+
#[utoipa::path(
856+
get,
857+
path = "/api/batch/download",
858+
params(
859+
("file_ids" = Option<String>, Query, description = "Comma-separated file IDs"),
860+
("folder_ids" = Option<String>, Query, description = "Comma-separated folder IDs"),
861+
),
862+
responses(
863+
(status = 200, description = "ZIP archive stream"),
864+
(status = 400, description = "Bad request"),
865+
(status = 401, description = "Unauthorized"),
866+
(status = 500, description = "ZIP creation failed")
867+
),
868+
tag = "batch"
869+
)]
870+
pub async fn download_batch_querystring(
871+
State(state): State<BatchHandlerState>,
872+
auth_user: AuthUser,
873+
Query(params): Query<BatchDownloadQuery>,
874+
) -> Result<Response, (StatusCode, String)> {
875+
process_download_batch(state, auth_user, params.into()).await
876+
}
835877
/// Handler for downloading multiple files and folders as a single ZIP.
836878
///
837879
/// The ZIP is written to a temporary file and streamed to the client,
@@ -847,10 +889,18 @@ pub async fn move_folders_batch(
847889
),
848890
tag = "batch"
849891
)]
850-
pub async fn download_batch(
892+
pub async fn download_batch_post(
851893
State(state): State<BatchHandlerState>,
852894
auth_user: AuthUser,
853895
Json(request): Json<BatchDownloadRequest>,
896+
) -> Result<Response, (StatusCode, String)> {
897+
process_download_batch(state, auth_user, request).await
898+
}
899+
900+
async fn process_download_batch(
901+
state: BatchHandlerState,
902+
auth_user: AuthUser,
903+
request: BatchDownloadRequest,
854904
) -> Result<Response, (StatusCode, String)> {
855905
if request.file_ids.is_empty() && request.folder_ids.is_empty() {
856906
return Err((

src/interfaces/api/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod cookie_auth;
2+
pub mod deserializer;
23
pub mod handlers;
34
pub mod routes;
45

@@ -130,7 +131,8 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
130131
handlers::batch_handler::get_folders_batch,
131132
handlers::batch_handler::move_folders_batch,
132133
handlers::batch_handler::trash_batch,
133-
handlers::batch_handler::download_batch,
134+
handlers::batch_handler::download_batch_post,
135+
handlers::batch_handler::download_batch_querystring,
134136
// Music/playlist handlers (free functions)
135137
handlers::music_handler::create_playlist,
136138
handlers::music_handler::list_playlists,

src/interfaces/api/routes.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,9 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
203203
// Trash operations (soft delete)
204204
.route("/trash", post(batch_handler::trash_batch))
205205
// Download as ZIP
206-
.route("/download", post(batch_handler::download_batch))
206+
.route("/download", post(batch_handler::download_batch_post))
207+
// work arround for drag & drop (does not support POST requests)
208+
.route("/download", get(batch_handler::download_batch_querystring))
207209
.with_state(batch_handler_state);
208210

209211
// Create search routes if the service is available

static/js/app/ui.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,41 @@ const ui = {
10811081
lastItemDiv = div;
10821082
}
10831083

1084+
let downloadUrl;
1085+
let nameEncoded;
1086+
1087+
// tells Browser URL to call to drop selection on operating system (desktop, file manager etc)
1088+
// will generate a zipfile if multiple
1089+
if (selectedCardFromList.length === 1) {
1090+
// only 1 file
1091+
if (info.type === 'file') {
1092+
nameEncoded = info.name.replaceAll(/:/g, '-'); // issue is that DownloadURL is using : as separator;
1093+
downloadUrl = `${window.location.origin}/api/files/${info.id}`;
1094+
} else {
1095+
// directory into ZIP
1096+
nameEncoded = info.name.replaceAll(/:/g, '-').concat('.zip');
1097+
downloadUrl = `${window.location.origin}/api/folders/${info.id}/download?format=zip`;
1098+
}
1099+
} else {
1100+
// must use ZIP container
1101+
// TODO better naming like ("selection in ${parent.name}") modulo i18n ? ...
1102+
const now = new Date().toISOString().replace(/T/, ' ').replace(/\.*/, '').replaceAll(/:/g, '-');
1103+
nameEncoded = `oxicloud ${now}.zip`;
1104+
const folders = [];
1105+
const files = [];
1106+
filesList.querySelectorAll(`div.selected`).forEach((e) => {
1107+
const item = itemInfo(e);
1108+
if (item.type === 'file') {
1109+
files.push(item.id);
1110+
} else {
1111+
folders.push(item.id);
1112+
}
1113+
});
1114+
downloadUrl = `${window.location.origin}/api/batch/download?file_ids=${files.join(',')}&folder_ids=${folders.join(',')}`;
1115+
}
1116+
1117+
e.dataTransfer?.setData('DownloadURL', `application/octet-stream:${nameEncoded}:${downloadUrl}`);
1118+
10841119
// if more than 1 item, display the badge
10851120
if (selectedCardFromList.length > 1) {
10861121
const badge = document.createElement('span');

static/sw.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// OxiCloud Service Worker
22
// FIXME: generate cache name according build ?
3-
const CACHE_NAME = 'oxicloud-cache-v20';
3+
const CACHE_NAME = 'oxicloud-cache-v21';
44

55
// Only cache static assets — NOT HTML files.
66
// HTML files are served network-first so browsers always get the latest

0 commit comments

Comments
 (0)