From 8e2ec9d69d3b82f3c71da25e0483c1c2760288bd Mon Sep 17 00:00:00 2001 From: ropali Date: Thu, 14 Nov 2024 17:22:44 +0530 Subject: [PATCH] feat: Container export option added --- src-tauri/src/commands/container.rs | 34 +++++++++- src-tauri/src/main.rs | 10 ++- src-tauri/src/utils/storage.rs | 23 ++++++- src/Icons/index.jsx | 18 +++++- .../Containers/ContainerDetails.jsx | 63 ++++++++++++++++++- 5 files changed, 136 insertions(+), 12 deletions(-) diff --git a/src-tauri/src/commands/container.rs b/src-tauri/src/commands/container.rs index 32c9fda..8bda07b 100644 --- a/src-tauri/src/commands/container.rs +++ b/src-tauri/src/commands/container.rs @@ -1,13 +1,15 @@ use crate::state::AppState; +use crate::utils::storage::get_user_download_dir; use crate::utils::terminal::{get_terminal, open_terminal}; -use bollard::container::{ - ListContainersOptions, LogsOptions, RenameContainerOptions, StatsOptions, -}; +use bollard::container::{ListContainersOptions, LogsOptions, RenameContainerOptions, StatsOptions}; use bollard::models::{ContainerInspectResponse, ContainerSummary}; use futures_util::StreamExt; use std::collections::HashMap; +use std::io::ErrorKind; use std::sync::atomic::Ordering; use tauri::Manager; +use tokio::fs::File; +use tokio::io::AsyncWriteExt; #[tauri::command] pub async fn fetch_containers( @@ -224,3 +226,29 @@ pub async fn rename_container( }) .map_err(|e| e.to_string()) } + +#[tauri::command] +pub async fn export_container(state: tauri::State<'_, AppState>, name: String) -> Result { + let download_dir = get_user_download_dir()?; + + let path = format!("{download_dir}/{name}.tar.gz"); + + let file_result = File::create_new::<&str>(path.as_ref()).await; + match file_result { + Ok(mut file) => { + let mut stream = state.docker.export_container(&name); + while let Some(response) = stream.next().await { + file.write_all(&response.unwrap()).await.unwrap(); + } + Ok(String::from(format!("Image exported at {}", path.clone()))) + } + Err(err) => { + let kind = err.kind(); + match kind { + ErrorKind::PermissionDenied => Err(String::from(format!("Permission denied to open target file: {path}"))), + ErrorKind::AlreadyExists => Err(String::from(format!("Target file already exist at {path}"))), + _ => Err(String::from(format!("Failed to open target file: {path}"))) + } + } + } +} \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0e7b344..b805159 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,15 +1,12 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use crate::commands::container::{ - container_operation, container_stats, fetch_container_info, fetch_containers, get_container, - rename_container, stream_docker_logs, -}; +use crate::commands::container::{container_operation, container_stats, export_container, fetch_container_info, fetch_containers, get_container, rename_container, stream_docker_logs}; use crate::commands::extra::{cancel_stream, get_version, ping}; use crate::commands::image::{delete_image, export_image, image_history, image_info, list_images}; use crate::commands::network::{inspect_network, list_networks}; -use crate::commands::volume::{inspect_volume, list_volumes}; use crate::commands::terminal::get_available_terminals; +use crate::commands::volume::{inspect_volume, list_volumes}; use crate::state::AppState; use crate::utils::storage::setup_storage; @@ -51,7 +48,8 @@ fn main() { get_version, ping, get_available_terminals, - rename_container + rename_container, + export_container, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/utils/storage.rs b/src-tauri/src/utils/storage.rs index 8b94752..548ec5d 100644 --- a/src-tauri/src/utils/storage.rs +++ b/src-tauri/src/utils/storage.rs @@ -1,4 +1,6 @@ -use std::path::PathBuf; +use std::env; +use std::env::VarError; +use std::path::{Path, PathBuf}; use tauri::{App, Manager, Wry}; use tauri_plugin_store::{with_store, StoreCollection}; @@ -14,6 +16,25 @@ pub fn get_user_home_dir() -> Option { } } +pub fn get_user_download_dir() -> Result { + let downloads_path = if cfg!(target_os = "windows") { + env::var("USERPROFILE") + .map(|profile| Path::new(&profile).join("Downloads")) + .map_err(|_| "Could not find USERPROFILE environment variable.".to_string())? + } else if cfg!(target_os = "macos") || cfg!(target_os = "linux") { + env::var("HOME") + .map(|home| Path::new(&home).join("Downloads")) + .map_err(|_| "Could not find HOME environment variable.".to_string())? + } else { + return Err("Unsupported operating system.".to_string()); + }; + + downloads_path + .to_str() + .map(|s| s.to_string()) + .ok_or_else(|| "Failed to convert path to string.".to_string()) +} + pub fn get_storage_path() -> PathBuf { let mut path = PathBuf::new(); diff --git a/src/Icons/index.jsx b/src/Icons/index.jsx index e9c8e9e..cf77618 100644 --- a/src/Icons/index.jsx +++ b/src/Icons/index.jsx @@ -490,4 +490,20 @@ export function IconCancel(props) { /> ) -} \ No newline at end of file +} + + +export function IconThreeDots(props) { + return ( + + + + ); +} diff --git a/src/components/Containers/ContainerDetails.jsx b/src/components/Containers/ContainerDetails.jsx index 5749e3f..3d7b972 100644 --- a/src/components/Containers/ContainerDetails.jsx +++ b/src/components/Containers/ContainerDetails.jsx @@ -3,7 +3,15 @@ import React, {useEffect, useState} from 'react'; import {listen} from '@tauri-apps/api/event'; import LogsViewer from '../LogsViewer'; -import {IconBxTerminal, IconBxTrashAlt, IconCircleStop, IconPlayCircle, IconRestart, IconWeb} from '../../Icons'; +import { + IconBxTerminal, + IconBxTrashAlt, + IconCircleStop, + IconPlayCircle, + IconRestart, + IconThreeDots, + IconWeb +} from '../../Icons'; import {useContainers} from '../../state/ContainerContext'; import LogoScreen from '../LogoScreen'; @@ -135,6 +143,41 @@ function ContainerDetails() { }); } + const exportContainer = () => { + setLoadingButton("export") + invoke('export_container', { + name: selectedContainer.Names[0].replace("/", ""), + + }).then((res) => { + if (res) { + toast.success(res); + + refreshSelectedContainer() + } + }).catch((e) => { + toast.error(e); + }).finally(() => { + setLoadingButton(null) + }); + } + + const downloadFromContainer = () => { + setLoadingButton("export") + invoke('download_from_container', { + name: selectedContainer.Names[0].replace("/", ""), + + }).then((res) => { + if (res) { + toast.success(res); + refreshSelectedContainer() + } + }).catch((e) => { + toast.error(e); + }).finally(() => { + setLoadingButton(null) + }); + } + const renderContent = () => { switch (activeTab) { case 'LOGS': @@ -216,6 +259,24 @@ function ContainerDetails() { } + +
+ {loadingButton == 'export' ? : + <> +
+ + + + } + +