From 2c71dfade6b57c617c418fea3735571a2a452291 Mon Sep 17 00:00:00 2001 From: Abdulla Abdurakhmanov Date: Wed, 14 Aug 2024 18:46:48 +0200 Subject: [PATCH] Clipboard source/destination integration --- Cargo.lock | 254 ++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/errors.rs | 5 + src/filesystems/clipboard.rs | 169 +++++++++++++++++++++++ src/filesystems/mod.rs | 15 +++ 5 files changed, 443 insertions(+), 1 deletion(-) create mode 100644 src/filesystems/clipboard.rs diff --git a/Cargo.lock b/Cargo.lock index 882fcbb..cf649d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,6 +140,24 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arboard" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb4009533e8ff8f1450a5bcbc30f4242a1d34442221f72314bea1f5dc9c7f89" +dependencies = [ + "clipboard-win", + "core-graphics", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "parking_lot", + "windows-sys 0.48.0", + "x11rb", +] + [[package]] name = "arg_enum_proc_macro" version = "0.3.4" @@ -798,6 +816,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + [[package]] name = "bstr" version = "1.10.0" @@ -992,6 +1019,15 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +[[package]] +name = "clipboard-win" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +dependencies = [ + "error-code", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -1065,6 +1101,30 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -1346,6 +1406,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "error-code" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" + [[package]] name = "exr" version = "1.72.0" @@ -1412,6 +1478,33 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1549,6 +1642,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -2148,7 +2251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -2391,6 +2494,105 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.6.0", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.6.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + [[package]] name = "object" version = "0.36.2" @@ -2435,6 +2637,29 @@ dependencies = [ "sha2", ] +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + [[package]] name = "paste" version = "1.0.15" @@ -2847,6 +3072,7 @@ dependencies = [ name = "redacter" version = "0.8.0" dependencies = [ + "arboard", "async-recursion", "async-trait", "aws-config", @@ -2884,6 +3110,15 @@ dependencies = [ "zip", ] +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "regex" version = "1.10.6" @@ -4368,6 +4603,23 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + [[package]] name = "xmlparser" version = "0.13.6" diff --git a/Cargo.toml b/Cargo.toml index b391757..dd9414f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ rand = "0.8" pdfium-render = { version = "0.8", features = ["thread_safe", "image"], optional = true } image = "0.25" bytes = { version = "1" } +arboard = { version = "3", features = ["image"] } [dev-dependencies] diff --git a/src/errors.rs b/src/errors.rs index c28fef4..aff5775 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,5 +1,6 @@ use gcloud_sdk::tonic::metadata::errors::InvalidMetadataValue; use indicatif::style::TemplateError; +use std::time::SystemTimeError; use thiserror::Error; #[derive(Error, Debug)] @@ -38,6 +39,10 @@ pub enum AppError { PdfiumError(#[from] pdfium_render::prelude::PdfiumError), #[error("Image conversion error: {0}")] ImageError(#[from] image::ImageError), + #[error("Clipboard error: {0}")] + ClipboardError(#[from] arboard::Error), + #[error("SystemTimeError: {0}")] + SystemTimeError(#[from] SystemTimeError), #[error("System error: {message}")] SystemError { message: String }, } diff --git a/src/filesystems/clipboard.rs b/src/filesystems/clipboard.rs new file mode 100644 index 0000000..22f57ca --- /dev/null +++ b/src/filesystems/clipboard.rs @@ -0,0 +1,169 @@ +use crate::errors::AppError; +use crate::filesystems::{ + AbsoluteFilePath, FileMatcher, FileSystemConnection, FileSystemRef, ListFilesResult, +}; +use crate::redacters::Redacters; +use crate::reporter::AppReporter; +use crate::AppResult; +use arboard::Clipboard; +use bytes::Bytes; +use futures::{Stream, TryStreamExt}; +use image::{ImageBuffer, ImageFormat}; +use rvstruct::ValueStruct; + +pub struct ClipboardFileSystem<'a> { + clipboard: Clipboard, + reporter: &'a AppReporter<'a>, +} + +impl<'a> ClipboardFileSystem<'a> { + pub async fn new(root_path: &str, reporter: &'a AppReporter<'a>) -> AppResult { + if root_path != "clipboard://" { + return Err(AppError::SystemError { + message: "Clipboard should be specified as clipboard://".into(), + }); + } + Ok(Self { + clipboard: Clipboard::new()?, + reporter, + }) + } +} + +impl<'a> FileSystemConnection<'a> for ClipboardFileSystem<'a> { + async fn download( + &mut self, + _file_ref: Option<&FileSystemRef>, + ) -> AppResult<( + FileSystemRef, + Box> + Send + Sync + Unpin + 'static>, + )> { + let filename = format!( + "{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs() + ); + match self.clipboard.get().image() { + Ok(image_data) => { + let maybe_image: Option = image::ImageBuffer::from_raw( + image_data.width as u32, + image_data.height as u32, + image_data.bytes.into_owned(), + ); + if let Some(image) = maybe_image { + let mut writer = std::io::Cursor::new(Vec::new()); + image.write_to(&mut writer, ImageFormat::Png)?; + let png_image_bytes = writer.into_inner(); + Ok(( + FileSystemRef { + relative_path: format!("{}.png", filename).into(), + media_type: Some(mime::TEXT_PLAIN), + file_size: Some(png_image_bytes.len() as u64), + }, + Box::new(futures::stream::iter(vec![Ok(bytes::Bytes::from( + png_image_bytes, + ))])), + )) + } else { + Err(AppError::SystemError { + message: "Clipboard can't get any supported image format from clipboard://" + .into(), + }) + } + } + Err(_) => { + let text = self.clipboard.get().text()?; + Ok(( + FileSystemRef { + relative_path: format!("{}.txt", filename).into(), + media_type: Some(mime::TEXT_PLAIN), + file_size: Some(text.len() as u64), + }, + Box::new(futures::stream::iter(vec![Ok(bytes::Bytes::from(text))])), + )) + } + } + } + + async fn upload> + Send + Unpin + Sync + 'static>( + &mut self, + input: S, + file_ref: Option<&FileSystemRef>, + ) -> AppResult<()> { + match file_ref { + Some(file_ref) => { + if let Some(mime) = file_ref.media_type.clone() { + let all_chunks: Vec = input.try_collect().await?; + let all_bytes = all_chunks.concat(); + if Redacters::is_mime_image(&mime) { + if let Some(image_format) = image::ImageFormat::from_mime_type(&mime) { + let image = + image::load_from_memory_with_format(&all_bytes, image_format)?; + + let image_width = image.width() as usize; + let image_height = image.height() as usize; + let image_buf: image::RgbaImage = ImageBuffer::from(image); + let raw = image_buf.into_raw(); + self.clipboard.set_image(arboard::ImageData { + width: image_width, + height: image_height, + bytes: raw.into(), + })?; + + Ok(()) + } else { + Err(AppError::SystemError { + message: "ClipboardFileSystem doesn't support this image format" + .into(), + }) + } + } else { + self.clipboard + .set_text(String::from_utf8_lossy(&all_bytes))?; + Ok(()) + } + } else { + Err(AppError::SystemError { + message: "ClipboardFileSystem requires MIME from source".into(), + }) + } + } + None => Err(AppError::SystemError { + message: "FileSystemRef is required for ClipboardFileSystem".into(), + }), + } + } + + async fn list_files( + &mut self, + _file_matcher: Option<&FileMatcher>, + ) -> AppResult { + self.reporter + .report("Listing in clipboard is not supported")?; + Ok(ListFilesResult::EMPTY) + } + + async fn close(self) -> AppResult<()> { + Ok(()) + } + + async fn has_multiple_files(&self) -> AppResult { + Ok(false) + } + + async fn accepts_multiple_files(&self) -> AppResult { + Ok(false) + } + + fn resolve(&self, file_ref: Option<&FileSystemRef>) -> AbsoluteFilePath { + AbsoluteFilePath { + file_path: format!( + "clipboard://{}", + file_ref + .map(|fr| fr.relative_path.value().to_string()) + .unwrap_or("".to_string()) + ), + } + } +} diff --git a/src/filesystems/mod.rs b/src/filesystems/mod.rs index cee1aa6..f60a0d8 100644 --- a/src/filesystems/mod.rs +++ b/src/filesystems/mod.rs @@ -20,7 +20,10 @@ mod gcs; mod local; mod zip; +mod clipboard; + use crate::filesystems::aws_s3::AwsS3FileSystem; +use crate::filesystems::clipboard::ClipboardFileSystem; use crate::reporter::AppReporter; #[derive(Debug, Clone, ValueStruct)] @@ -95,6 +98,7 @@ pub enum DetectFileSystem<'a> { GoogleCloudStorage(GoogleCloudStorageFileSystem<'a>), AwsS3(AwsS3FileSystem<'a>), ZipFile(ZipFileSystem<'a>), + Clipboard(ClipboardFileSystem<'a>), } impl<'a> DetectFileSystem<'a> { @@ -118,6 +122,10 @@ impl<'a> DetectFileSystem<'a> { return Ok(DetectFileSystem::ZipFile( ZipFileSystem::new(file_path, reporter).await?, )); + } else if file_path.starts_with("clipboard://") { + return Ok(DetectFileSystem::Clipboard( + ClipboardFileSystem::new(file_path, reporter).await?, + )); } else { Err(AppError::UnknownFileSystem { file_path: file_path.to_string(), @@ -139,6 +147,7 @@ impl<'a> FileSystemConnection<'a> for DetectFileSystem<'a> { DetectFileSystem::GoogleCloudStorage(fs) => fs.download(file_ref).await, DetectFileSystem::AwsS3(fs) => fs.download(file_ref).await, DetectFileSystem::ZipFile(fs) => fs.download(file_ref).await, + DetectFileSystem::Clipboard(fs) => fs.download(file_ref).await, } } @@ -152,6 +161,7 @@ impl<'a> FileSystemConnection<'a> for DetectFileSystem<'a> { DetectFileSystem::GoogleCloudStorage(fs) => fs.upload(input, file_ref).await, DetectFileSystem::AwsS3(fs) => fs.upload(input, file_ref).await, DetectFileSystem::ZipFile(fs) => fs.upload(input, file_ref).await, + DetectFileSystem::Clipboard(fs) => fs.upload(input, file_ref).await, } } @@ -164,6 +174,7 @@ impl<'a> FileSystemConnection<'a> for DetectFileSystem<'a> { DetectFileSystem::GoogleCloudStorage(fs) => fs.list_files(file_matcher).await, DetectFileSystem::AwsS3(fs) => fs.list_files(file_matcher).await, DetectFileSystem::ZipFile(fs) => fs.list_files(file_matcher).await, + DetectFileSystem::Clipboard(fs) => fs.list_files(file_matcher).await, } } @@ -173,6 +184,7 @@ impl<'a> FileSystemConnection<'a> for DetectFileSystem<'a> { DetectFileSystem::GoogleCloudStorage(fs) => fs.close().await, DetectFileSystem::AwsS3(fs) => fs.close().await, DetectFileSystem::ZipFile(fs) => fs.close().await, + DetectFileSystem::Clipboard(fs) => fs.close().await, } } @@ -182,6 +194,7 @@ impl<'a> FileSystemConnection<'a> for DetectFileSystem<'a> { DetectFileSystem::GoogleCloudStorage(fs) => fs.has_multiple_files().await, DetectFileSystem::AwsS3(fs) => fs.has_multiple_files().await, DetectFileSystem::ZipFile(fs) => fs.has_multiple_files().await, + DetectFileSystem::Clipboard(fs) => fs.has_multiple_files().await, } } @@ -191,6 +204,7 @@ impl<'a> FileSystemConnection<'a> for DetectFileSystem<'a> { DetectFileSystem::GoogleCloudStorage(fs) => fs.accepts_multiple_files().await, DetectFileSystem::AwsS3(fs) => fs.accepts_multiple_files().await, DetectFileSystem::ZipFile(fs) => fs.accepts_multiple_files().await, + DetectFileSystem::Clipboard(fs) => fs.accepts_multiple_files().await, } } @@ -200,6 +214,7 @@ impl<'a> FileSystemConnection<'a> for DetectFileSystem<'a> { DetectFileSystem::GoogleCloudStorage(fs) => fs.resolve(file_ref), DetectFileSystem::AwsS3(fs) => fs.resolve(file_ref), DetectFileSystem::ZipFile(fs) => fs.resolve(file_ref), + DetectFileSystem::Clipboard(fs) => fs.resolve(file_ref), } } }