Skip to content

Commit

Permalink
Merge pull request #2 from dcodesdev/dev
Browse files Browse the repository at this point in the history
Handle single file copies
  • Loading branch information
dcodesdev authored May 11, 2024
2 parents 441bcfe + d1926e5 commit 97a3d72
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 103 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "fast-scp"
version = "0.1.1"
version = "0.1.2"
description = "A fast and simple scp CLI tool"
license = "MIT"
repository = "https://github.com/dcodesdev/fast-scp"
Expand Down
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# scp-rs
# Fast SCP

**WARNING: THIS CLI TOOL IS STILL IN BETA AND NOT READY FOR USE**

A Rust CLI tool to copy files from remote server to local machine or the other way around, handles tasks concurrently, which makes it faster than the traditional scp command.
A Rust CLI tool to copy files from your linux remote server to your local machine. Each file is ran on a separate thread, which makes it much faster than the traditional `scp` command.

## Example

Expand Down
2 changes: 2 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ impl From<ssh2::Error> for ScpError {
ScpError::Ssh(e)
}
}

pub type Result<T = ()> = anyhow::Result<T, ScpError>;
85 changes: 43 additions & 42 deletions src/scp.rs
Original file line number Diff line number Diff line change
@@ -1,49 +1,70 @@
use futures::future::join_all;
use indicatif::{ProgressBar, ProgressStyle};
use ssh2::Session;
use ssh2::{Session, Sftp};
use std::{
ffi::OsStr,
fs::{self, File},
io::{Read, Write},
net::TcpStream,
path::PathBuf,
time::Duration,
};

use crate::{error::ScpError, utils::with_retry};
use crate::{error::Result, utils::with_retry};

pub struct Connect {
session: Session,
ssh_opts: SshOpts,
mode: Mode,
sftp: Sftp,
}

impl Connect {
pub fn new(ssh_opts: SshOpts, mode: Mode) -> anyhow::Result<Self, ScpError> {
pub fn new(ssh_opts: SshOpts, mode: Mode) -> Result<Self> {
let session = create_session(&ssh_opts)?;
let sftp = session.sftp()?;

Ok(Self {
session,
ssh_opts,
mode,
sftp,
})
}

pub async fn receive(&self, from: &PathBuf, to: &PathBuf) -> anyhow::Result<(), ScpError> {
let start = std::time::Instant::now();
pub async fn receive(&self, from: &PathBuf, to: &PathBuf) -> Result<()> {
let is_dir = self.stat(from)?;

let files = self.list(from)?;
if is_dir {
self.handle_dir(from, to).await
} else {
self.handle_file(from, to).await
}
}

async fn handle_file(&self, from: &PathBuf, to: &PathBuf) -> Result<()> {
let full_path = to.join(from.file_name().unwrap_or(OsStr::new("unknown")));
let result =
copy_file_from_remote(&self.ssh_opts, from.clone(), full_path, &self.mode).await;

println!("✅ File received successfully");
result
}

async fn handle_dir(&self, from: &PathBuf, to: &PathBuf) -> Result<()> {
let files = self.list_files(from)?;
let pb = ProgressBar::new(files.len() as u64);
pb.set_style(
ProgressStyle::with_template(
"{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({eta})",
"{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({eta})\n\n{msg}",
)
.unwrap()
.progress_chars("#>-"),
);
pb.enable_steady_tick(Duration::from_millis(100));

let mut handles = Vec::new();
for item in files {
for item in &files {
let to_path = to.join(item.strip_prefix(from).unwrap());
let item_clone = item.clone();
let ssh_opts = self.ssh_opts.clone();
Expand All @@ -62,7 +83,10 @@ impl Connect {
let items = join_all(handles).await;

if items.iter().all(|x| x.is_ok()) {
println!("\nDone in {:.2?}", start.elapsed());
pb.finish_with_message(format!(
"✅ All files received successfully ({} files)",
files.len()
));
Ok(())
} else {
Err(std::io::Error::new(
Expand All @@ -73,10 +97,15 @@ impl Connect {
}
}

fn list(&self, dir: &PathBuf) -> anyhow::Result<Vec<PathBuf>, ScpError> {
fn stat(&self, path: &PathBuf) -> Result<bool> {
let file = self.sftp.stat(&path)?;
Ok(file.is_dir())
}

fn list_files(&self, dir: &PathBuf) -> Result<Vec<PathBuf>> {
let mut channel = self.session.channel_session()?;

channel.exec(&format!("ls -R {}", dir.display()))?;
channel.exec(&format!("find {} -type f", dir.display()))?;

let mut buf = String::new();
channel.read_to_string(&mut buf)?;
Expand All @@ -88,35 +117,7 @@ impl Connect {
}

pub fn find_files(buf: &str) -> Vec<PathBuf> {
let mut dirs: Vec<PathBuf> = Vec::new();
let structured = buf
.split("\n\n")
.map(|x| {
let mut lines = x.lines();
let dir: PathBuf = lines.next().unwrap().split(":").next().unwrap().into();

let files = lines.collect::<Vec<_>>();

let full_path = files
.iter()
.map(|x| PathBuf::new().join(x))
.map(|x| dir.join(x))
.collect::<Vec<_>>();

dirs.push(dir);
full_path
})
.collect::<Vec<_>>();

let flattened = structured.iter().flatten().collect::<Vec<_>>();

let files_only = flattened
.iter()
.filter(|x| !dirs.contains(x))
.map(|x| x.to_path_buf())
.collect::<Vec<_>>();

files_only
buf.lines().map(|line| PathBuf::from(line.trim())).collect()
}

#[derive(Clone)]
Expand All @@ -140,7 +141,7 @@ async fn copy_file_from_remote(
remote_file_path: PathBuf,
local_file_path: PathBuf,
mode: &Mode,
) -> anyhow::Result<(), ScpError> {
) -> Result<()> {
let create_session = || create_session(ssh_opts);
let session = with_retry(create_session, 10)?;

Expand Down Expand Up @@ -176,7 +177,7 @@ async fn copy_file_from_remote(
Ok(())
}

pub fn create_session(ssh_opts: &SshOpts) -> anyhow::Result<Session, ScpError> {
pub fn create_session(ssh_opts: &SshOpts) -> Result<Session> {
// Connect to the host
let tcp = TcpStream::connect(&ssh_opts.host)?;
let mut session = Session::new()?;
Expand Down
27 changes: 9 additions & 18 deletions tests/snapshots/files__tests__find_files.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,13 @@ assertion_line: 18
expression: files_only
---
[
"project/README.md",
"project/package.json",
"project/src/app/App.tsx",
"project/src/app/index.ts",
"project/src/components/Header.tsx",
"project/src/components/Footer.tsx",
"project/src/components/Sidebar.tsx",
"project/src/components/index.ts",
"project/src/hooks/useAuth.ts",
"project/src/hooks/useFetch.ts",
"project/src/hooks/index.ts",
"project/src/utils/helpers.ts",
"project/src/utils/constants.ts",
"project/src/utils/index.ts",
"project/tests/App.test.tsx",
"project/tests/Header.test.tsx",
"project/tests/Footer.test.tsx",
"project/tests/Sidebar.test.tsx",
"my-dir/file.js",
"my-dir/js/file.js",
"my-dir/js/file2.js",
"my-dir/rust/src/main.rs",
"my-dir/rust/Cargo.toml",
"my-dir/rust/Cargo.lock",
"my-dir/rust/target/debug/my-dir",
"my-dir/css/style.css",
"my-dir/css/other/style.css",
]
46 changes: 9 additions & 37 deletions tests/snapshots/ls-R.log
Original file line number Diff line number Diff line change
@@ -1,37 +1,9 @@
project:
src
tests
README.md
package.json

project/src:
app
components
hooks
utils

project/src/app:
App.tsx
index.ts

project/src/components:
Header.tsx
Footer.tsx
Sidebar.tsx
index.ts

project/src/hooks:
useAuth.ts
useFetch.ts
index.ts

project/src/utils:
helpers.ts
constants.ts
index.ts

project/tests:
App.test.tsx
Header.test.tsx
Footer.test.tsx
Sidebar.test.tsx
my-dir/file.js
my-dir/js/file.js
my-dir/js/file2.js
my-dir/rust/src/main.rs
my-dir/rust/Cargo.toml
my-dir/rust/Cargo.lock
my-dir/rust/target/debug/my-dir
my-dir/css/style.css
my-dir/css/other/style.css

0 comments on commit 97a3d72

Please sign in to comment.