透過 shell 傳送檔案

現代 linux 主機幾乎必備 ssh 提供遠端登入功能, 要傳送檔案的話,ssh 有提供 sftp 模組。 但就算沒有 ftp,還是可以透過 shell 重導向 直接傳送單一檔案,或用 base64 之類程式 把二進位檔案編碼成純文字再傳送。 如果要傳送多個資料夾, 則可以用 tar 把資料夾和檔案的樹狀結構 序列化成單一的 stream 來傳送。

遠端登入外再架一個檔案分享服務?

基本上架好 linux,除非是當日常使用, 不然當伺服器用的話,就會先裝好遠端登入的服務。 最基礎就是 ssh,習慣圖形介面的可能裝 vnc rdp teamviewer。 有些要和 windows 互通,就會裝 samba, 可以共享檔案,或也可以裝 ftp。

sftp 與 scp

但其實如果用 ssh,就有內建的 sftp 可以用, 去改 /etc/ssh/sshd.conf 打開就行了。 有 sftp 就能像 ftp 傳送檔案, 甚至用 gvfs-mount 可以掛載起來。 只是 windows 原生不支援 sftp, 要用 filezilla 或其它程式。

現在 windows 10 支援 ssh 後,也多了 sftp 的命令列工具, 但 ftp 可以用 檔案總管 開,sftp 還是不行, 而且 sftp 好像沒有處理編碼的問題, 我複製回去 utf8 的檔名會變亂碼, windows 的檔案記得是 big5。

一般有 sftp 就有 scp, sftp 介面和 ftp 基本一樣,也可以寫成腳本。 scp 則是簡化版,像 cp 一樣一個命令複製的。 二個只要有存 ssh-key 都可以免密碼登入。

有關 samba

samba 就是 unix 下的 smb, 但其實我一直不會用 smb,也不太知道 smb 能幹麻。 家裡二台電腦好像有用 smb 連線過,高中電腦課也有用來交過作業。 (照老師要求把作業檔案複製到網路上的芳鄰中某個資料夾中。) 但我開始正式學電腦就是 linux, 對 windows 下的 smb 也就沒接觸過。

感覺 smb 的功能就是簡單的分享檔案, 無需另外設定,在同一區網下就能互連, 還能直接用檔案總管開。 我認為在 unix 下就該用 ftp, 反正 gvfs 用 fuse 虛擬檔案系統把 ftp 掛載到本機下, 就能像打開本機端檔案一樣, 直接用檔案管理員或編輯器打開遠端 ftp 上的資料夾檔案讀寫。

只是對一間公司而言,大部份人都用 windows, 那 linux 伺服器架個 samba, 讓所有人直接存取是最簡單的作法。

在 shell 中傳送單一檔案

最簡單的就是傳送單一文字檔, 用 ssh 或其它遠端登入後,打開 vi 或 nano 進入編輯, 然後把本地的檔案內容整個複製,在 ssh vi 中貼上就好了。 進階一點會用 cat > my.txt , 貼上後再 ctrl-d 就會把內容直接輸入到檔案裡。

二進位編碼為純文字

但編輯器大多只能傳送純文字, 就算用 cat 可以傳送二進位,剪貼簿也不一定能存二進位。 這時候就能用 base64,用 base64 編碼把二進位資料轉成純文字, 之後就能存到剪貼簿了。

# 在本地的 shell 把圖片編碼成 base64
base64 image.png > image.png.base64
# 在遠端把 base64 還原成圖片
base64 -d image.png.base64 > origin.png # 把 base64 轉回來

base64 會把編碼輸出到 stdout, 如果不重導向就是直接顯示在終端機裡, 可以直接選取複製。 解碼就加 -d 選項,預設也是從 stdin 讀, 貼上後按 ctrl-d 就好了; 注意記得把解碼結果重導向, 不然二進位顯示在終端機會亂碼。

另外除了 base64,也可以用 xxd 來編碼。 base64 會把三個 byte 編成四個字元, xxd 則是把一個 byte 編成二個字元。

## 編碼與解碼
xxd image.png | xxd -r > origin.png
## 省去行數與明文
xxd -p image.png | xxd -r -p > origin.png

直接 pipe 給遠端命令

上面的作法都是經由剪貼簿,直接在終端機把純文字送過去。 但其實剪貼簿沒那麼大,要送太大的檔案不太方便。 最好是可以直接 pipe 給遠端。 這就要提到 ssh 其實是 security shell 的縮寫, 也就是一種 shell,所以可以直接指定要執行的命令。

例如 ssh user@host ls 就是登入後執行 ls 就退出。 而這個執行結果,當然可以 pipe 給其它程式, 甚至也可以把本機的程式 pipe 給 ssh 的遠端程式, 像這樣:

cat my.txt.gz | ssh user@host zcat >my.txt

要注意,遠端的 zcat 輸出是會送回本機, 再被 shell 重導向到本機的檔案; 如果要把檔案存到遠端,要把 > 跳脫, 才能把 > 傳給遠端的 shell。

管道直接相連後可以安全傳送二進位資料

上面的 base64,其實可以直接放在 ssh 後面, 只是之前用 base64 是要方便剪下貼上, 現在雙方直接用管道連起來了,就不用再特地編碼成純文字, 直接用 cat 就好了。

## 相比 cat,base64 多作一次編解碼,沒什麼意義。
cat image.png | ssh user@host cat \> image.png
base64 image.png | ssh user@host base64 -d \> image.png

然後如果是用 cat,就可以直接把輸出導向到檔案, 類似複製的意思。 這裡一定要用 cat 的原因是,從本機送過去是送到遠端的 stdin, 但 shell 只能把 stdout 導到檔案, 所以需要一支程式把 stdin 轉到 stdout, 也就是 cat 了。

cat my.txt.gz | ssh user@host zcat \> my.txt
cat my.txt | ssh user@host 'cat >my.txt'
ssh user@host cat \>my.txt <my.txt

scat

早期的 scp,聽說就只是把 ssh cat 用 shell script 包裝一下, 後來才改寫成完整的一支程式的。 但 scp 有個問題,他不能把遠端的檔案輸出到 stdout, 只能直接複製成檔案。 有時候不想多複製一次檔案,但要 ssh user@host cat file 又很麻煩, 所以我就按照 scp 的格式,做了一個 scat。

# 一樣意思的命令
ssh user@host cat file | cksum
scat user@host:file - | cksum # 輸出遠端的 file 到本機計算校檢合
scat user@host:file | cksum # 第二個 `-` 可省

# 一樣意思的命令
cat file | ssh user@host cat \> file
cat file | scat - user@host:file # 把 stdin 導向到遠端的 file
scat - user@host:file <file # 與上面同義

偶爾會用到,然後 ssh 連線大部份都有預設用 gzip 壓縮, 所以如果要傳送大檔案,可以不用考慮頻寬先壓縮,ssh 會幫你壓好。 (主要看你的 .ssh/config/etc/ssh/ssh_config 有沒有 Compression yes 。)

用 tar 傳送目錄樹狀結構

以上說到怎麼傳送一個 stream。 但以目前的作法,都只能用 cat 傳送單一檔案, 那要怎麼傳送多個檔案呢? 只要把多個檔案、目錄結構變成單一檔案就能傳了。 在 windows 下就是用 zip, 在 linux 也有 zip,但因為 unix 下打包和壓縮是分離的二個工具, 打包的工具是 tar,而上面提到 ssh 基本已經有壓縮功能,所以用 tar 就夠了。

tar -C $HOME -cf - . | ssh user@host tar -xf f # 把整個家目錄打包丟過去
ssh user@host tar -cf - | tar -xf - # 丟回來

diff patch

另外有一個有趣的東西叫 diff, 用來比較二個文字檔、打 patch 的工具。 diff 可以遞迴比較子目錄下所有檔案,產生一個大補丁, 對不存在的檔案,如果選擇當作空檔案,就會把整個檔案記錄下來。 一般是用來比較二個不同版本的原始碼目錄, 產生一個補丁檔,之後就能用該補丁, 把一個版本變成另一版本。

如果你把一個目錄和一個空目錄或不存在的目錄比較, 就能把整個目錄打包成一個 diff 檔, patch 該 diff 就能憑空產生出原有的目錄。

# 把 .ssh 目錄打包成一個 diff,送到 host 上
diff --new-file --unified --recursive --text not-exist .ssh \
    | ssh user@host patch --strip=0

其中 diff 的行為有點怪, 預設的行為不能遞迴 patch 整個資料夾, 要 --unifined--context 才可以。 --new-file 是將不存在的檔案視為空檔案, --text 則是將所有檔案都視為文字檔, 也就是對二進位檔也照樣 diff。

adb 也可以通

上面都是用 ssh 遠端登入傳送, 其實像 android 的 adb 也可以用類似的功能。 adb shell 可以連線到 android 上開啟一個 shell, adb shell ls 則是連進去執行 ls 就退出, 所以只要用 tar 命令,就能簡單把資料夾整個送過去。

# 在 android 上開起 shell
adb shell

# 在 android 上執行 df 命令後退出
adb shell df

# 把 pictures 資料夾整個送過去
tar -czf - pictures | adb shell tar -xzf -

# 備份 sd 卡裡的所有資料
adb shell tar -czf - /sdcard >sdcard.tgz

我記得 adb 的連線不像 ssh,是沒有壓縮的。 所以建議手動壓縮,會快一點; 除非你手機太爛 cpu 壓縮和解壓縮很慢。

只透過 stdin 傳送指令與檔案

以上的作法都是建立在 ssh 可以直接指定執行的程式, 並把資料直接送到該程式的 stdin。 但如果遠端無法指定執行的程式呢? 像 telnet,只能登入進 shell,沒辦法指定命令, 那就只能把檔案嵌在腳本裡,把整個腳本餵給 telnet 了。

用 here doc 嵌入檔案

之前也有提過這種作法, 把檔案嵌入在腳本的 here doc 裡, 如果是二進位檔案可能要先 base64 編碼。 也就是產生一個腳本長這樣的:

base64 -d >file <<EOF
5aaC5p6c5piv5LqM6YCy5L2N5qqU5qGI5Y+v6IO96KaB5YWIYmFzZTY057eo56K844CCCuS5n+Ww
seaYr+eUoueUn+S4gOWAi+iFs+acrOmVt+mAmeaoo+eahO+8mgo=
EOF

然後把整個腳本 pipe 給 telnet,登入後執行。

結合上面那篇文章中的技巧,我們可以用命令群組產生腳本, 而不用把腳本存成檔案後再 pipe 過去:

# 直接用命令產生
{
    echo "base64 -d >file <<EOF"
    base64 file
    echo "EOF"
} | telnet host

# 用 here doc 產生
cat <<CAT | telnet host
base64 -d >file <<BASE64
$(base64 file)
BASE64
CAT

其中二種作法,我是較推薦第一種, 第二種嵌套了二層 here doc,不好閱讀。 而且把整個 base64 編碼的檔案用命令代換進 here doc, here doc 會變很長,對 shell 的負擔有點大。

但其實第一種作法也是把整個檔案送到遠端的 here doc 裡, shell 在處理 here doc 上 必須全部 buffer 起來後才能送出, 一樣會造成很大負擔。 只是第一種是只有遠端 buffer 一次,第二種是本機也要 buffer 一次。

當然,也可以一次傳送多個檔案, 原本是直接 base64 編碼檔案, 改成 tar 打包後再 pipe 給 base64 就好。

{
    echo "base64 -d | tar -xf - <<EOF"
    tar -cf - . | base64
    echo "EOF"
} | telnet host

直接使用 stdin 不使用 here doc

最後再提一個我自己也不太清楚的用法。 在 shell 中如果直接用 cat,會嘗試從標準輸入讀取, 這時候就能直接從 shell 輸入,按 ctrl-d 可以退出。 我記得以前也聽過一個建議, 盡量不要把腳本 pipe 給 shell, 而是把腳本存成檔案,再用 sh script.sh 的方式執行。 因為像 cat 這種命令會需要讀取 stdin, 但 pipe 給 shell 時 stdin 已經被腳本佔住了, 那就不可能再從 stdin 讀取。

如果在 shell script 檔案中呼叫 cat, 就會從執行時的 stdin 讀取, 所以 pipe 給執行的 script 是可以的:

cat file | sh script.sh
cat file | perl script.pl

但如果 script 是在 stdin,那讀取時會發生什麼事? 可能是接下來 stdin 都被當成該程式的輸入, 或是該程式讀 stdin 時直接讀到 eof。 我發現 dash 和 bash 的行為不同, bash 會把接下來的內容輸入給程式,dash 則直接 eof。 可以用下列程式來驗證,如果 eof tr 則不會有輸出, ls 也會被執行。如果 ls 被當作 tr 的輸入, 那就會輸出大寫的 LS

dash <<SHELL
tr a-z A-Z
ls
SHELL

bash <<SHELL
tr a-z A-Z
ls
SHELL

總之,這也可以用來傳送檔案, 只是要確定遠端的 shell 是 bash, zsh 好像也支援?

{
    echo 'tar -xf -'
    tar -cf - ./
} | ssh user@host

只是我對背後的原理還不是很確定。 比 here doc 好是不需要 shell buffer 所有資料, 也不需要 base64 編碼,可以直接用 shell 傳送二進位資料。