diff --git a/daemon/src/files/batch.rs b/daemon/src/files/batch.rs new file mode 100644 index 00000000..ed699c3d --- /dev/null +++ b/daemon/src/files/batch.rs @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: AGPL-3.0-only +use crate::config::StorageEntry; +use crate::files::copy::copy_file_locked; +use crate::files::delete::delete_file_locked; +use crate::files::list::list_files_locked; +use crate::files::mkdir::mkdir_file_locked; +use crate::files::rename::rename_file_locked; +use crate::files::stat::stat_file_locked; +use crate::files::write::write_file_by_url_locked; +use crate::storage::StorageIdentifier; +use crate::storage::sync::{StorageLockData, StorageTokens, storage_read_timeout}; +use crate::{Daemon, DaemonExtra, StorageNotification}; +use daemon_common::service::error_storage_failure::FromStorageFailure; +use daemon_common::service::error_storage_mutex_locked::FromStorageMutexLocked; +use daemon_common::service::error_storage_not_found::FromStorageNotFound; +use daemon_common::service::files::task::{FileOperationKind, StorageTask}; +use daemon_common::service::files::{ + BatchOperationError, FileOperation, FileOperationBatch, FileOperationFailure, + FileOperationResult, UrlWriteBatch, UrlWriteResult, +}; +use futures::StreamExt as _; +use std::io; +use tokio::sync::broadcast::Sender; + +const DEFAULT_PARALLEL: u8 = 8; +const MAX_PARALLEL: u8 = 16; + +/// Cancel safe +pub async fn write_files_by_url< + E: From + + From + + FromStorageNotFound + + FromStorageMutexLocked + + FromStorageFailure, +>( + daemon: &Daemon, + id: StorageIdentifier, + batch: UrlWriteBatch, +) -> Result, E> { + let StorageLockData { + lock, + storage, + notify, + tokens, + .. + } = storage_read_timeout::( + &daemon.config, + &*daemon.conn, + &daemon.storages, + id, + StorageTask::FileOperation { + kind: FileOperationKind::WriteByUrlBatch, + }, + ) + .await?; + + let parallel = usize::from( + batch + .parallel + .unwrap_or(DEFAULT_PARALLEL) + .clamp(1, MAX_PARALLEL), + ); + let (entry, token_map, sender) = (&storage, &tokens, ¬ify); + let results = futures::stream::iter(batch.files) + .map(|file| async move { + let error = write_file_by_url_locked::( + daemon, + id, + entry, + token_map, + sender, + file.path.clone(), + file.url, + ) + .await + .err() + .map(FileOperationFailure::from); + UrlWriteResult { + path: file.path, + error, + } + }) + .buffered(parallel) + .collect::>() + .await; + + drop(lock); + Ok(results) +} + +async fn run_operation( + id: StorageIdentifier, + storage: &StorageEntry, + tokens: &StorageTokens, + notify: &Sender, + operation: FileOperation, +) -> Result { + let mut result = FileOperationResult::default(); + match operation { + FileOperation::List { path } => { + result.entries = + Some(list_files_locked::(id, storage, tokens, path).await?); + } + FileOperation::Stat { path } => { + result.entry = + Some(stat_file_locked::(id, storage, tokens, path).await?); + } + FileOperation::Delete { path, recursive } => { + delete_file_locked::(id, storage, tokens, notify, path, recursive) + .await?; + } + FileOperation::Rename { from, to } => { + rename_file_locked::(id, storage, tokens, notify, from, to) + .await?; + } + FileOperation::Copy { from, to } => { + copy_file_locked::(id, storage, tokens, notify, from, to).await?; + } + FileOperation::Mkdir { path, recursive } => { + mkdir_file_locked::(id, storage, tokens, notify, path, recursive) + .await?; + } + } + Ok(result) +} + +/// Cancel safe +pub async fn file_operations< + E: From + + From + + FromStorageNotFound + + FromStorageMutexLocked + + FromStorageFailure, +>( + daemon: &Daemon, + id: StorageIdentifier, + batch: FileOperationBatch, +) -> Result, E> { + let StorageLockData { + lock, + storage, + notify, + tokens, + .. + } = storage_read_timeout::( + &daemon.config, + &*daemon.conn, + &daemon.storages, + id, + StorageTask::FileOperation { + kind: FileOperationKind::Batch, + }, + ) + .await?; + + let mut results = Vec::with_capacity(batch.operations.len()); + let mut failed = false; + for operation in batch.operations { + if failed && batch.stop_on_error { + results.push(FileOperationResult { + error: Some(FileOperationFailure::Skipped), + ..FileOperationResult::default() + }); + continue; + } + + let result = run_operation(id, &storage, &tokens, ¬ify, operation) + .await + .unwrap_or_else(|err| FileOperationResult { + error: Some(err.into()), + ..FileOperationResult::default() + }); + failed |= result.error.is_some(); + results.push(result); + } + + drop(lock); + Ok(results) +} diff --git a/daemon/src/files/copy.rs b/daemon/src/files/copy.rs index 6ee4b58d..deaea68c 100644 --- a/daemon/src/files/copy.rs +++ b/daemon/src/files/copy.rs @@ -1,7 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-only +use crate::config::StorageEntry; use crate::files::storage_path; use crate::storage::StorageIdentifier; -use crate::storage::sync::{StorageLockData, storage_read_timeout}; +use crate::storage::sync::{StorageLockData, StorageTokens, storage_read_timeout}; use crate::utils::storage_parent_ino; use crate::{Daemon, DaemonExtra, StorageNotification}; use cap_async_std::fs::MetadataExt as _; @@ -11,6 +12,7 @@ use daemon_common::service::error_storage_not_found::FromStorageNotFound; use daemon_common::service::files::task::{FileOperationKind, StorageTask}; use std::io; use std::io::ErrorKind; +use tokio::sync::broadcast::Sender; use tracing::{debug, error, warn}; /// Cancel safe @@ -43,8 +45,22 @@ pub async fn copy_file< }, ) .await?; - let (storage_from, _, path_from) = storage_path(id, &storage, from, &tokens).await?; - let (storage_to, _, path_to) = storage_path(id, &storage, to, &tokens).await?; + let result = copy_file_locked::(id, &storage, &tokens, ¬ify, from, to).await; + drop(lock); + result +} + +/// Cancel safe, the caller holds the storage lock +pub(super) async fn copy_file_locked>( + id: StorageIdentifier, + storage: &StorageEntry, + tokens: &StorageTokens, + notify: &Sender, + from: String, + to: String, +) -> Result<(), E> { + let (storage_from, _, path_from) = storage_path(id, storage, from, tokens).await?; + let (storage_to, _, path_to) = storage_path(id, storage, to, tokens).await?; let to_parent_ino = storage_parent_ino(&storage_to, &path_to).await; @@ -77,6 +93,5 @@ pub async fn copy_file< }); } - drop(lock); Ok(()) } diff --git a/daemon/src/files/delete.rs b/daemon/src/files/delete.rs index 8854f9e1..89d68e83 100644 --- a/daemon/src/files/delete.rs +++ b/daemon/src/files/delete.rs @@ -1,7 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-only +use crate::config::StorageEntry; use crate::files::storage_path; use crate::storage::StorageIdentifier; -use crate::storage::sync::{StorageLockData, storage_read_timeout}; +use crate::storage::sync::{StorageLockData, StorageTokens, storage_read_timeout}; use crate::utils::storage_parent_ino; use crate::{Daemon, DaemonExtra, StorageNotification}; use cap_async_std::fs::MetadataExt as _; @@ -11,6 +12,7 @@ use daemon_common::service::error_storage_not_found::FromStorageNotFound; use daemon_common::service::files::task::{FileOperationKind, StorageTask}; use std::io; use std::io::ErrorKind; +use tokio::sync::broadcast::Sender; use tracing::{debug, error, warn}; /// Cancel safe @@ -43,7 +45,21 @@ pub async fn delete_file< }, ) .await?; - let (storage, _, path) = storage_path(id, &storage, path, &tokens).await?; + let result = delete_file_locked::(id, &storage, &tokens, ¬ify, path, recursive).await; + drop(lock); + result +} + +/// Cancel safe, the caller holds the storage lock +pub(super) async fn delete_file_locked>( + id: StorageIdentifier, + storage: &StorageEntry, + tokens: &StorageTokens, + notify: &Sender, + path: String, + recursive: bool, +) -> Result<(), E> { + let (storage, _, path) = storage_path(id, storage, path, tokens).await?; let ino = storage.metadata(&path).await.map(|meta| meta.ino()); let parent_ino = storage_parent_ino(&storage, &path).await; @@ -80,6 +96,5 @@ pub async fn delete_file< }); } - drop(lock); Ok(()) } diff --git a/daemon/src/files/list.rs b/daemon/src/files/list.rs index 152a6744..1a38ea25 100644 --- a/daemon/src/files/list.rs +++ b/daemon/src/files/list.rs @@ -1,8 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-only +use crate::config::StorageEntry; use crate::files::{IntoFileEntry as _, storage_path}; use crate::storage::StorageIdentifier; use crate::storage::archive::format::ArchiveFormat; -use crate::storage::sync::{StorageLockData, storage_read_timeout}; +use crate::storage::sync::{StorageLockData, StorageTokens, storage_read_timeout}; use crate::{Daemon, DaemonExtra}; use daemon_common::service::error_storage_failure::FromStorageFailure; use daemon_common::service::error_storage_mutex_locked::FromStorageMutexLocked; @@ -43,7 +44,19 @@ pub async fn list_files< }, ) .await?; - let (storage, _, path) = storage_path(id, &storage, path, &tokens).await?; + let result = list_files_locked::(id, &storage, &tokens, path).await; + drop(lock); + result +} + +/// Cancel safe, the caller holds the storage lock +pub(super) async fn list_files_locked>( + id: StorageIdentifier, + storage: &StorageEntry, + tokens: &StorageTokens, + path: String, +) -> Result, E> { + let (storage, _, path) = storage_path(id, storage, path, tokens).await?; let read_dir = storage .read_dir(&path) .await @@ -97,6 +110,5 @@ pub async fn list_files< archive, )); } - drop(lock); Ok(files) } diff --git a/daemon/src/files/mkdir.rs b/daemon/src/files/mkdir.rs index 837f7e2a..f170ab10 100644 --- a/daemon/src/files/mkdir.rs +++ b/daemon/src/files/mkdir.rs @@ -1,7 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-only +use crate::config::StorageEntry; use crate::files::storage_path; use crate::storage::StorageIdentifier; -use crate::storage::sync::{StorageLockData, storage_read_timeout}; +use crate::storage::sync::{StorageLockData, StorageTokens, storage_read_timeout}; use crate::utils::storage_parent_ino; use crate::{Daemon, DaemonExtra, StorageNotification}; use cap_async_std::fs::MetadataExt as _; @@ -11,6 +12,7 @@ use daemon_common::service::error_storage_not_found::FromStorageNotFound; use daemon_common::service::files::task::{FileOperationKind, StorageTask}; use std::io; use std::io::ErrorKind; +use tokio::sync::broadcast::Sender; use tracing::{debug, error, warn}; /// Cancel safe @@ -43,7 +45,21 @@ pub async fn mkdir_file< }, ) .await?; - let (storage, _, path) = storage_path(id, &storage, path, &tokens).await?; + let result = mkdir_file_locked::(id, &storage, &tokens, ¬ify, path, recursive).await; + drop(lock); + result +} + +/// Cancel safe, the caller holds the storage lock +pub(super) async fn mkdir_file_locked>( + id: StorageIdentifier, + storage: &StorageEntry, + tokens: &StorageTokens, + notify: &Sender, + path: String, + recursive: bool, +) -> Result<(), E> { + let (storage, _, path) = storage_path(id, storage, path, tokens).await?; let parent_ino = storage_parent_ino(&storage, &path).await; @@ -85,6 +101,5 @@ pub async fn mkdir_file< }); } - drop(lock); Ok(()) } diff --git a/daemon/src/files/mod.rs b/daemon/src/files/mod.rs index cdf8755f..f31f056b 100644 --- a/daemon/src/files/mod.rs +++ b/daemon/src/files/mod.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-only mod archive; +mod batch; mod chmod; mod chown; mod copy; @@ -19,6 +20,7 @@ use crate::storage::sync::StorageTokens; use crate::storage::{STORAGE_TOKEN_MAGIC, StorageIdentifier}; use crate::utils::path_relative; pub use archive::{create_archive, unpack_archive}; +pub use batch::{file_operations, write_files_by_url}; use cap_async_std::ambient_authority; use cap_async_std::fs::{Dir, FileTypeExt as _, Metadata, MetadataExt as _}; pub use chmod::chmod_file; diff --git a/daemon/src/files/rename.rs b/daemon/src/files/rename.rs index 771e4543..51a26a5c 100644 --- a/daemon/src/files/rename.rs +++ b/daemon/src/files/rename.rs @@ -1,7 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-only +use crate::config::StorageEntry; use crate::files::storage_path; use crate::storage::StorageIdentifier; -use crate::storage::sync::{StorageLockData, storage_read_timeout}; +use crate::storage::sync::{StorageLockData, StorageTokens, storage_read_timeout}; use crate::utils::storage_parent_ino; use crate::{Daemon, DaemonExtra, StorageNotification}; use cap_async_std::fs::MetadataExt as _; @@ -11,6 +12,7 @@ use daemon_common::service::error_storage_not_found::FromStorageNotFound; use daemon_common::service::files::task::{FileOperationKind, StorageTask}; use std::io; use std::io::ErrorKind; +use tokio::sync::broadcast::Sender; use tracing::{debug, error, warn}; /// Cancel safe @@ -43,8 +45,22 @@ pub async fn rename_file< }, ) .await?; - let (storage_from, _, path_from) = storage_path(id, &storage, from, &tokens).await?; - let (storage_to, _, path_to) = storage_path(id, &storage, to, &tokens).await?; + let result = rename_file_locked::(id, &storage, &tokens, ¬ify, from, to).await; + drop(lock); + result +} + +/// Cancel safe, the caller holds the storage lock +pub(super) async fn rename_file_locked>( + id: StorageIdentifier, + storage: &StorageEntry, + tokens: &StorageTokens, + notify: &Sender, + from: String, + to: String, +) -> Result<(), E> { + let (storage_from, _, path_from) = storage_path(id, storage, from, tokens).await?; + let (storage_to, _, path_to) = storage_path(id, storage, to, tokens).await?; let from_parent_ino = storage_parent_ino(&storage_from, &path_from).await; let to_parent_ino = storage_parent_ino(&storage_to, &path_to).await; @@ -84,6 +100,5 @@ pub async fn rename_file< warn!("Rename was successful, but unable to trigger storage notification: {err:?}"); }); } - drop(lock); Ok(()) } diff --git a/daemon/src/files/stat.rs b/daemon/src/files/stat.rs index c08d8d0c..e6e26227 100644 --- a/daemon/src/files/stat.rs +++ b/daemon/src/files/stat.rs @@ -1,8 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-only +use crate::config::StorageEntry; use crate::files::{IntoFileEntry as _, storage_path}; use crate::storage::StorageIdentifier; use crate::storage::archive::format::ArchiveFormat; -use crate::storage::sync::{StorageLockData, storage_read_timeout}; +use crate::storage::sync::{StorageLockData, StorageTokens, storage_read_timeout}; use crate::{Daemon, DaemonExtra}; use daemon_common::service::error_storage_failure::FromStorageFailure; use daemon_common::service::error_storage_mutex_locked::FromStorageMutexLocked; @@ -44,7 +45,19 @@ pub async fn stat_file< }, ) .await?; - let (storage, _, path) = storage_path(id, &storage, path, &tokens).await?; + let result = stat_file_locked::(id, &storage, &tokens, path).await; + drop(lock); + result +} + +/// Cancel safe, the caller holds the storage lock +pub(super) async fn stat_file_locked + FromFileNotFound>( + id: StorageIdentifier, + storage: &StorageEntry, + tokens: &StorageTokens, + path: String, +) -> Result { + let (storage, _, path) = storage_path(id, storage, path, tokens).await?; let metadata = storage .symlink_metadata(&path) .await @@ -93,7 +106,6 @@ pub async fn stat_file< } let relative_path = path.join(filename.clone()); - drop(lock); Ok(metadata.into_fileentry( filename, relative_path.to_string_lossy().to_string(), diff --git a/daemon/src/files/write.rs b/daemon/src/files/write.rs index 9d8916f8..4f4844a5 100644 --- a/daemon/src/files/write.rs +++ b/daemon/src/files/write.rs @@ -1,7 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-only +use crate::config::StorageEntry; use crate::files::storage_path; use crate::storage::StorageIdentifier; -use crate::storage::sync::{StorageLockData, storage_read_timeout}; +use crate::storage::sync::{StorageLockData, StorageTokens, storage_read_timeout}; use crate::utils::storage_parent_ino; use crate::{Daemon, DaemonExtra, StorageNotification}; use cap_async_std::fs::{Dir, MetadataExt as _, OpenOptions}; @@ -223,18 +224,39 @@ pub async fn write_file_by_url< }, ) .await?; - let (storage, _, path) = storage_path(id, &storage, path, &tokens).await?; + let result = + write_file_by_url_locked::(daemon, id, &storage, &tokens, ¬ify, path, url).await; + drop(lock); + result +} + +/// Cancel safe, the caller holds the storage lock +pub(super) async fn write_file_by_url_locked< + E: From + + FromHttpRequestFailed + + FromHttpRequestNotSuccess + + FromHttpRequestReadFailed + + Send + + 'static, +>( + daemon: &Daemon, + id: StorageIdentifier, + storage: &StorageEntry, + tokens: &StorageTokens, + notify: &Sender, + path: String, + url: Url, +) -> Result<(), E> { + let (storage, _, path) = storage_path(id, storage, path, tokens).await?; tokio::task::spawn(write_file_by_url0::( daemon.clone(), storage, path, - notify, + notify.clone(), url, )) .await - .map_err(>::from)??; - drop(lock); - Ok(()) + .map_err(>::from)? } #[inline(always)] @@ -286,7 +308,7 @@ async fn write_file_by_url0< .map_err(|_err| ::from())? { writer - .write(&chunk) + .write_all(&chunk) .await .inspect_err(|err| match err.kind() { ErrorKind::QuotaExceeded => debug!("Failed to write file by url: {err:?}"), diff --git a/daemon/src/service/files.rs b/daemon/src/service/files.rs index d4be273a..c07f3e53 100644 --- a/daemon/src/service/files.rs +++ b/daemon/src/service/files.rs @@ -2,23 +2,26 @@ use std::collections::HashSet; // SPDX-License-Identifier: AGPL-3.0-only use crate::files::{ - chmod_file, chown_file, copy_file, create_archive, delete_file, download_file, list_files, - mkdir_file, rename_file, stat_file, symlink_file, unpack_archive, watch_files, wipe, - write_file, write_file_by_template, write_file_by_url, + chmod_file, chown_file, copy_file, create_archive, delete_file, download_file, file_operations, + list_files, mkdir_file, rename_file, stat_file, symlink_file, unpack_archive, watch_files, + wipe, write_file, write_file_by_template, write_file_by_url, write_files_by_url, }; use crate::storage::StorageIdentifier; use crate::{Daemon, DaemonExtra}; use async_trait::async_trait; use axum::extract::WebSocketUpgrade; -use daemon_common::service::files::FileEntry; use daemon_common::service::files::FilesService; use daemon_common::service::files::files_service_error::{ ServerChmodError, ServerChownError, ServerCopyFileError, ServerCreateArchiveError, ServerCreateDirectoryError, ServerDeleteFileError, ServerDownloadFileError, - ServerDownloadFileUserError, ServerListFilesError, ServerRenameFileError, ServerStatFileError, - ServerSymlinkError, ServerUnpackArchiveError, ServerUploadFileError, ServerUploadFileUserError, - ServerWatchFilesError, ServerWatchFilesUserError, ServerWriteFileByTemplateError, - ServerWriteFileByUrlError, WipeServerError, + ServerDownloadFileUserError, ServerFileOperationsError, ServerListFilesError, + ServerRenameFileError, ServerStatFileError, ServerSymlinkError, ServerUnpackArchiveError, + ServerUploadFileError, ServerUploadFileUserError, ServerWatchFilesError, + ServerWatchFilesUserError, ServerWriteFileByTemplateError, ServerWriteFileByUrlError, + ServerWriteFilesByUrlError, WipeServerError, +}; +use daemon_common::service::files::{ + FileEntry, FileOperationBatch, FileOperationResult, UrlWriteBatch, UrlWriteResult, }; use daemon_common::service::server::ServerFileWrite; use daemon_common::service::{DataStream, FileResponse, WebsocketResponse}; @@ -133,6 +136,24 @@ impl FilesService for Daemon { write_file_by_url(self, storage, path, url).await } + async fn server_write_files_by_url( + &self, + server_id: u32, + batch: UrlWriteBatch, + ) -> Result, ServerWriteFilesByUrlError> { + let storage = StorageIdentifier::Primary(server_id); + write_files_by_url(self, storage, batch).await + } + + async fn server_file_operations( + &self, + server_id: u32, + batch: FileOperationBatch, + ) -> Result, ServerFileOperationsError> { + let storage = StorageIdentifier::Primary(server_id); + file_operations(self, storage, batch).await + } + async fn server_write_file_by_template( &self, server_id: u32, diff --git a/daemon_common/src/service/files.rs b/daemon_common/src/service/files.rs index d59d00e9..c9dc1723 100644 --- a/daemon_common/src/service/files.rs +++ b/daemon_common/src/service/files.rs @@ -226,6 +226,34 @@ pub trait FilesService { verify: UserSignature, ) -> Result; + /// Writes many files from urls in one call + /// + /// Runs the same write as `GET /url` for every file, up to `parallel` at a time, so a + /// caller with many files needs one request instead of one per file. Every file gets its + /// own result at the same index, one failed download does not stop the others. + #[route("/url/batch", method = POST)] + #[error(storage::{not_found, mutex_locked, failure <- [io:*]}, sql::failure <- [sql:*])] + async fn server_write_files_by_url( + &self, + server_id: u32, + batch: UrlWriteBatch, + ) -> Result, _>; + + /// Runs many file operations in one call + /// + /// Runs the operations in order, each one exactly like its own route (`/list`, `/stat`, + /// `DELETE /file`, `/rename`, `/copy`, `/mkdir`), so a caller with many operations needs + /// one request instead of one per operation. Every operation gets its own result at the + /// same index. With `stop_on_error` the remaining operations are skipped after the first + /// failure. + #[route("/batch", method = POST)] + #[error(storage::{not_found, mutex_locked, failure <- [io:*]}, sql::failure <- [sql:*])] + async fn server_file_operations( + &self, + server_id: u32, + batch: FileOperationBatch, + ) -> Result, _>; + /// Deletes all files #[route("/wipe", method = GET)] #[error(storage::{not_found, mutex_locked, failure <- [io:*]}, sql::failure <- [sql:*])] @@ -346,3 +374,114 @@ pub enum FilesWatchResponseEntry { offset: i64, }, } + +error!( + BatchOperationError, + storage::{failure <- [io:*], quota_reached <- [io:QuotaExceeded]}, + file::{ + not_found <- [io:NotFound], + sandbox_violated <- [io:PermissionDenied], + already_exists <- [io:AlreadyExists], + not_directory <- [io:NotADirectory], + is_directory <- [io:IsADirectory] + }, + http_request::{failed, not_success, read_failed} +); + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct UrlWrite { + pub path: String, + #[schema(value_type = String)] + pub url: Url, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct UrlWriteBatch { + pub files: Vec, + #[serde(default)] + pub parallel: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(tag = "op", rename_all = "snake_case")] +pub enum FileOperation { + List { + path: String, + }, + Stat { + path: String, + }, + Delete { + path: String, + #[serde(default)] + recursive: bool, + }, + Rename { + from: String, + to: String, + }, + Copy { + from: String, + to: String, + }, + Mkdir { + path: String, + #[serde(default)] + recursive: bool, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct FileOperationBatch { + pub operations: Vec, + #[serde(default)] + pub stop_on_error: bool, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum FileOperationFailure { + StorageFailure, + QuotaReached, + NotFound, + SandboxViolated, + AlreadyExists, + NotDirectory, + IsDirectory, + HttpRequestFailed, + HttpRequestNotSuccess, + HttpRequestReadFailed, + Skipped, +} + +impl From for FileOperationFailure { + fn from(value: BatchOperationError) -> Self { + match value { + BatchOperationError::StorageFailure | BatchOperationError::Communication(_) => { + Self::StorageFailure + } + BatchOperationError::StorageQuotaReached => Self::QuotaReached, + BatchOperationError::FileNotFound => Self::NotFound, + BatchOperationError::FileSandboxViolated => Self::SandboxViolated, + BatchOperationError::FileAlreadyExists => Self::AlreadyExists, + BatchOperationError::FileNotDirectory => Self::NotDirectory, + BatchOperationError::FileIsDirectory => Self::IsDirectory, + BatchOperationError::HttpRequestFailed => Self::HttpRequestFailed, + BatchOperationError::HttpRequestNotSuccess => Self::HttpRequestNotSuccess, + BatchOperationError::HttpRequestReadFailed => Self::HttpRequestReadFailed, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct UrlWriteResult { + pub path: String, + pub error: Option, +} + +#[derive(Clone, Default, Serialize, Deserialize, utoipa::ToSchema)] +pub struct FileOperationResult { + pub error: Option, + pub entry: Option, + pub entries: Option>, +} diff --git a/daemon_common/src/service/files/task.rs b/daemon_common/src/service/files/task.rs index 946a0971..74e1d05d 100644 --- a/daemon_common/src/service/files/task.rs +++ b/daemon_common/src/service/files/task.rs @@ -64,7 +64,9 @@ pub enum FileOperationKind { Symlink, Write, WriteByUrl, + WriteByUrlBatch, WriteByTemplate, + Batch, } #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]