feat(ui): permit drag&drop to the operating system

a drop outside of the browser will:
  - upload the file if only 1 file selected
  - upload a .zip of the directory or multiple selection (browsers do not permit multiple upload yet)

note: I had to create a new handler because a post request to /api/batch/download is possible via JS but it will create a memory blob in the browser, during the drag action.

This may exhaust the browser's memory if heavy files
This will initiate zip creation from the server even if drop is canceled
The best approach is to add a handler supporting GET calls, this call will be triggered by the browser on drop action outside of it's window
This commit is contained in:
Edouard Vanbelle
2026-04-28 13:15:08 +02:00
parent fbd59a1f38
commit 05037e7491
5 changed files with 54 additions and 2 deletions
+14
View File
@@ -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
View File
@@ -1,4 +1,5 @@
pub mod cookie_auth;
pub mod deserializer;
pub mod handlers;
pub mod routes;
+3 -1
View File
@@ -203,7 +203,9 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
// Trash operations (soft delete)
.route("/trash", post(batch_handler::trash_batch))
// 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);
// Create search routes if the service is available
+35
View File
@@ -1081,6 +1081,41 @@ const ui = {
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 (selectedCardFromList.length > 1) {
const badge = document.createElement('span');
+1 -1
View File
@@ -1,6 +1,6 @@
// OxiCloud Service Worker
// 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.
// HTML files are served network-first so browsers always get the latest