Skip to content

Commit

Permalink
feat: Container export option added
Browse files Browse the repository at this point in the history
  • Loading branch information
ropali committed Nov 14, 2024
1 parent cecc3c1 commit 8e2ec9d
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 12 deletions.
34 changes: 31 additions & 3 deletions src-tauri/src/commands/container.rs
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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<String, String> {
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}")))
}
}
}
}
10 changes: 4 additions & 6 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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");
Expand Down
23 changes: 22 additions & 1 deletion src-tauri/src/utils/storage.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -14,6 +16,25 @@ pub fn get_user_home_dir() -> Option<String> {
}
}

pub fn get_user_download_dir() -> Result<String, String> {
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();
Expand Down
18 changes: 17 additions & 1 deletion src/Icons/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -490,4 +490,20 @@ export function IconCancel(props) {
/>
</svg>
)
}
}


export function IconThreeDots(props) {
return (
<svg
fill="currentColor"
viewBox="0 0 16 16"
height="1em"
width="1em"
{...props}
>
<path
d="M3 9.5a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm5 0a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm5 0a1.5 1.5 0 110-3 1.5 1.5 0 010 3z"/>
</svg>
);
}
63 changes: 62 additions & 1 deletion src/components/Containers/ContainerDetails.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -216,6 +259,24 @@ function ContainerDetails() {
<IconBxTrashAlt className="size-5"/>}
</button>
</div>

<div className="dropdown dropdown-bottom">
{loadingButton == 'export' ? <button className="btn btn-square btn-sm mr-3">

<span className="loading loading-bars loading-xs"></span>
</button> :
<>
<div tabIndex={0} role="button" className="btn btn-square btn-sm mr-3"><IconThreeDots
className="size-5"/></div>

<ul tabIndex={0}
className="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
<li><a onClick={() => exportContainer()}>Export Container</a></li>
</ul>
</>
}

</div>
</div>
<div className="flex mb-4 border-b border-base-300">
<button className={`mr-4 pb-2 ${activeTab === 'LOGS' ? 'border-b-2 border-base-content' : ''}`}
Expand Down

0 comments on commit 8e2ec9d

Please sign in to comment.