問題的提出

公司開發機與遠端伺服器之間有嚴格的隔離策略,不能直接使用 ssh 登入,而必需通過跳板機。這樣一來,本地與伺服器之間的一些檔案傳輸變得非常不便。經過諮詢,運維教了我一招:

$ nc -l 8080 > filename
$ nc yunhai.xxxxxxxx.xxxxx.com 8080 < filename

使用 nc 建立連線來傳輸檔案,第一行命令在伺服器執行,第二行命令在本地執行,它的引數就是遠端伺服器的 host,執行完成後,將檔案從本地拷貝到遠端。這樣確實可以工作,而且 8080 是運維允許通行的埠,上面的命令是合規的。反過來想將檔案從遠端拷貝到本地,只需要將上面的重定向符換個方向即可,順序不變,仍是先在伺服器執行第一行命令:

$ nc -l 8080 < filename
$ nc yunhai.xxxxxxxx.xxxxx.com 8080 > filename

從伺服器拷貝大量經過選擇的檔案,運維還教了另外一個辦法來實現:

$ python -m SimpleHTTPServer 8080

本地開啟瀏覽器訪問 yunhai.xxxxxxxx.xxxxx.com:8080 就能實現類似 ftp 一樣的功能啦:

凡是在這條命令啟動目錄下面的檔案,都可以通過點選上面的連結下載,子目錄的話則會展開。與 nc 不同,每次下載不再是“一錘子買賣”,你可以一直下一直下……這又一次體現了 python 的強大 (雖然我不怎麼用)。

這篇文章寫到這裡似乎就可以結束了,然而我要說的是,上面的工具都不能滿足我的需求。因為我不只是需要一個跨跳板機傳輸檔案的工具 (其實用 secretcrt 的 rz/sz 就挺好,不過我司未購買,禁止員工安裝盜版),還想要一個跨多臺機器儲存和共享檔案的機制。例如我本身是在 mac 上開發,還有一臺 windows 測試筆記本,遠端 linux 伺服器目前有一臺,但是將來很可能會擴充套件……想想將來要在這麼多機器上找到並傳輸一個檔案我就頭大。

就在我一愁莫展的時候,安全組的同事提供了一個基於企業網盤的命令列工具,可以通過命令列的方式上傳下載檔案,在 mac 上還有桌面端可以用。首先它是合規的,其次它上傳、下載的檔案位於企業網盤你的個人賬戶下的一個特定目錄,其它人沒有許可權看不到,而你在開發機上通過瀏覽器登入時,如果之前已經登入過公司的帳號,就會 SSO 無感登入:

然後就可以在瀏覽器裡檢視、編輯、上傳、下載檔案啦。另一方面,在伺服器使用命令列也可以 SSO 免登入直接上傳下載:

$ bst_tool --help
當前使用者:yunhai Bxxxx Secure Transmission tool. Version x.x.x.x. Usage:
bst_tool COMMAND [flags] [options] where COMMAND is one of:
ls 列出指定目錄下的檔案
get 下載企業網盤中的檔案到本地
put 上傳本地檔案到企業網盤
delete 刪除企業網盤中的檔案
version 顯示當前版本號 Find more detail by:
bst_tool COMMAND -help

這個工具濃縮的都是精華,只提供列出 (ls)、下載 (get)、上傳 (put)、刪除 (delete) 這四大基本操作,首先來看 ls:

$ bst_tool ls /init
當前使用者:yunhai
total size: 7
│─────────────│──────────│──────────│─────────────────────│──────│─────────────────────│
│ FILEID (7) │ USERNAME │ FILESIZE │ TIME │ TYPE │ FILENAME │
│─────────────│──────────│──────────│─────────────────────│──────│─────────────────────│
│ 406869750 │ yunhai │ 535.00B │ 2021-04-13 21:24:00 │ file │ init_bst.sh │
│ 406850107 │ yunhai │ 820.00B │ 2021-04-13 19:35:24 │ file │ create_user.sh │
│ 406842658 │ yunhai │ 14.28KB │ 2021-04-13 19:28:05 │ file │ bash_bst.txt │
│ 404998873 │ yunhai │ 409.00B │ 2021-04-07 20:25:16 │ file │ build_cmake.sh │
│ 402750998 │ yunhai │ 15.73MB │ 2021-03-31 16:24:10 │ file │ cmake-3.20.0.zip │
│ 402361471 │ yunhai │ 289.00B │ 2021-03-30 20:41:32 │ file │ build_glibc.sh │
│ 402359162 │ yunhai │ 2.85MB │ 2021-03-30 20:33:41 │ file │ global-6.6.5.tar.gz │
│─────────────│──────────│──────────│─────────────────────│──────│─────────────────────│

如果給 ls 的引數是一個檔案,那麼它只列出這個檔案的詳細資訊:

$ bst_tool ls /init/create_user.sh
當前使用者:yunhai
total size: 1
│───────────│──────────│──────────│─────────────────────│──────│────────────────│
│ FILEID │ USERNAME │ FILESIZE │ TIME │ TYPE │ FILENAME │
│───────────│──────────│──────────│─────────────────────│──────│────────────────│
│ 406850107 │ yunhai │ 820.00B │ 2021-04-13 19:35:24 │ file │ create_user.sh │
│───────────│──────────│──────────│─────────────────────│──────│────────────────│

至於遞迴列出目錄中的子目錄什麼的,想都不要想了,沒有。get 每次可以下載一個檔案:

$ bst_tool get /init/create_user.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/cnblogs/create_user.sh

上面這個指令碼就是我在新機器上必跑的一個指令碼,用來建立非 root 使用者,並修改一些常用的配置。如果你本地已經有一個同名檔案,bst 會貼心的給出提示:

$ bst_tool get /init/create_user.sh
當前使用者:yunhai
錯誤:本地路徑 /home/yunh/code/cnblogs/create_user.sh 指向一個已存在檔案,請先自行確認並刪除已存在檔案,或者選擇其他路徑

這種情況下是不會覆蓋本地檔案的,防止意外丟失資料。如果就是要覆蓋,那麼先刪除一下就好啦~

有新的檔案需要上傳時,可以使用 put 子命令:

$ bst_tool put score.txt /data/
當前使用者:yunhai
uploading... 100.00 %
upload success:
FileId: 415268968
FilePath: /data/score.txt
Time: 2021-05-06 15:54:31

如果不指定目標檔名,自動使用本地檔名;如果指定的目標目錄不存在,自動遞迴建立路徑中的每個目錄;那如果遠端目標已經存在了呢?

$ bst_tool put create_user.sh /init/
當前使用者:yunhai
uploading... 100.00 %
upload success:
FileId: 415278480
FilePath: /init/create_user(1).sh
Time: 2021-05-06 16:05:29

好傢伙,自動重新命名了,命名方式是名稱後跟序號,這也是一種保護資料的思路。

一些臨時檔案用完以後,可以刪除:

$ bst_tool delete /data/score.txt
當前使用者:yunhai
deleting the 1th filePath: /data/score.txt
delete result: 成功

在介紹的過程中,我相信你已經瞭解到了這個小工具的幾個先天不足:

  1. 不支援遞迴列出,想要知道一個檔案有沒有在遠端目錄、在哪個子目錄下面,很難;
  2. 不支援遞迴下載,想要一次性下載一個目錄,很難;
  3. 不支援覆蓋下載,想要將遠端檔案備份到本地固定目錄,很難;
  4. 不支援覆蓋上傳,想要將一個檔案的修改版本上傳到同一個位置,很難。

作為日常使用的一部分,能用是不夠的,必需要好用!作為 shell 資深使用者,看不慣就改是我們的座右銘,這次就拿它來開刀~

問題的解決

柿子先檢軟的捏,這個覆蓋上傳、覆蓋下載看起來挺容易,通過預先探測來得知檔案是否存在,如果存在了給使用者一個告警,並讓使用者 (就是我) 選擇是放棄還是繼續覆蓋就可以了,當用戶選擇覆蓋時,將已存在後臺的檔案先刪除,再上傳,完事兒~

檔案是否存在

在正式開始之前,我們先看一下針對以下幾種情況,bst_tool ls 的效果,因為這個會影響我們後面的判斷邏輯。

  • 目錄存在且不為空
  • 檔案存在
  • 目錄存在但為空
  • 檔案或目錄不存在
$ bst_tool ls /data
當前使用者:yunhai
total size: 1
│───────────│──────────│──────────│─────────────────────│──────│──────────│
│ FILEID │ USERNAME │ FILESIZE │ TIME │ TYPE │ FILENAME │
│───────────│──────────│──────────│─────────────────────│──────│──────────│
│ 419245020 │ yunhai │ 283.00B │ 2021-05-14 15:44:24 │ file │ data │
│───────────│──────────│──────────│─────────────────────│──────│──────────│
$ bst_tool ls /data/data
當前使用者:yunhai
total size: 1
│───────────│──────────│──────────│─────────────────────│──────│──────────│
│ FILEID │ USERNAME │ FILESIZE │ TIME │ TYPE │ FILENAME │
│───────────│──────────│──────────│─────────────────────│──────│──────────│
│ 419245020 │ yunhai │ 283.00B │ 2021-05-14 15:44:24 │ file │ data │
│───────────│──────────│──────────│─────────────────────│──────│──────────│
$ bst_tool ls /empty
當前使用者:yunhai
total size: 0
$ bst_tool ls /data/data.txt
當前使用者:yunhai
total size: 0

這裡分別列出了 /data 目錄、/data/data 檔案、/empty 目錄、/data/data.txt 檔案,最後一個是不存在的檔案。可以看到前兩組和後兩組的差別比較大,關鍵字就是 total size: 0 這行,為 0 表示不存在或空目錄,不為 0 表示存在;但是這樣就萬事大吉了嗎?今天這個例子我舉得比較巧,相信細心的人已經看出來了,/data 目錄和 /data/data 檔案的輸出完全一樣!巧就巧在它們名稱相同、而且目錄下只有一個同名檔案,這種場景下,第二個 ls 是輸出檔案的詳細資訊;第一個 ls 是輸出目錄下的檔案的詳細資訊、而它剛好就是這個檔案,所以輸出內容是難辨彼此。

現在關鍵點就聚焦在一個項到底是檔案還是目錄了,可能有的人會說,用 TYPE 欄位唄,然而上面的例子中 TYPE 都是 file 的區分不出來。當然啦,一般場景下目錄會有多個檔案,totoal size > 1  的話必然是一個目錄,只是這樣並不嚴謹。現在考慮一下 bst 工具中還有什麼能幫到我,除了 ls 外就剩 put / get / delete,三種了,put 是我們要執行的命令,delete 是萬萬不行滴,於是考查一下 get 呢:

$ bst_tool get /data/data
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/data
$ bst_tool get /data
當前使用者:yunhai
no file named 'data' found in directory '/'

哈哈,真金不怕火煉,這一下目錄露餡了 —— get 只能下載檔案。可以通過試下載到臨時目錄來判斷到底是檔案還是目錄,這不失為一個終極手段。於是就有了下面這段指令碼:

 1 # $1: dest path
2 #
3 # return value:
4 # 0: not exist
5 # 1: file exist
6 # 2: dir exist but empty
7 # 3: dir exist and not empty
8 bsttool_query_path()
9 {
10 local dst=$1
11 local res=$(bst_tool ls "$dst")
12 local line=$(echo "$res" | sed -n '2p')
13 local str=${line/"total size"//}
14 local download="/tmp/bst.tmp"
15 local ret=0
16 if [ "$str" != "$line" ]; then
17 # normal return
18 ret=$(echo "$line" | awk '{print $NF}')
19 if [ $ret -eq 0 ]; then
20 ret=2
21 elif [ $ret -gt 1 ]; then
22 ret=3 # a dir
23 else # size=1
24 # note here we can NOT using type file/dir to determine,
25 # for example, there is a path /tmp/tmp pointed to a file
26 # when we ls /tmp, it report one item named 'tmp' with type file,
27 # but actually it is a dir.
28 # so here we use another way to determine, that is, download it,
29 # you can NOT download a dir.
30 echo "dir or file exist, try download to check"
31 rm "$download" > /dev/null 2>&1 # otherwise bst_tool will fail
32 line=$(bst_tool get "$dst" "$download" | sed -n '2p')
33 str=${line/"download success"//}
34 if [ "$str" != "$line" ]; then # string replace changed: download success
35 # is a file
36 ret=1
37 else
38 # is a dir
39 ret=3
40 fi
41 fi
42 else
43 # server response error, no such path
44 ret=0
45 fi
46
47 return $ret
48 }

下面做個簡單說明:

  • line 13,16 : 判斷是否訪問 server 出錯
  • line 18:提取 total size 大小
  • line 19-20:為 0 表示不存在或空目錄
  • line 21-22:大於 1 為非空目錄
  • line 31-32:試下載,為防止下載衝突,提前清理臨時檔案
  • lline 33:提取 download success 關鍵資訊
  • line 34-36: 下載成功,檔案
  • line 37-39:下載失敗,目錄
  • line 43-44:server 訪問出錯,可以認為檔案不存在

注意這裡驗證一行中是否包含特定字串的方法,使用了 shell 字串替換語法:$(line/"character string"//),如果找到並替換成功 (替換為空其實就是刪除),得到的字串肯定會和原串不同;否則沒有變化。

上面這些邏輯封裝成 shell 函式 bsttool_get_path,呼叫時提供一個路徑引數、然後根據返回值來判斷一個路徑的屬性。不過這個演算法有個不太好的地方,就是遇到上面那種分辨不清的場景而待檢查檔案又特別大時,需要消耗不少無謂的網路頻寬和時間,但是也沒有辦法,工具本身提供的功能和資訊太少了,我們這邊可以做的優化就是:不要一上來就用這個試下載去判斷,而是其它方法都已經窮盡、迫不得已時才請它出山。

覆蓋上傳

能確認檔案屬性後,就可以開始正文了,再回顧一下上一節中待上傳檔案可能的四種狀態及對覆蓋上傳的影響:

  • 檔案存在:提示使用者
  • 檔案或目錄不存在:直接上傳
  • 目錄存在但為空:直接上傳,最終檔案將位於目錄下
  • 目錄存在且不為空:需要繼續判斷目錄下有無同名檔案
    • 檔案存在:提示使用者
    • 檔案或目錄不存在:直接上傳
    • 目錄存在:直接上傳,最終將和該目錄並列位於父目錄之下
 1 # $1: dest path
2 #
3 # return value
4 # 0: user allow and delete existing remote file
5 # 1: user disallow and do nothing
6 bsttool_duplicate_warning()
7 {
8 local resp
9 echo -n "dest file exist, do you want to cover it? (n/y) "
10 read resp
11 case "$resp" in
12 "y"|"Y")
13 # remove remote reource first
14 bst_tool delete "$1"
15 return 0
16 ;;
17 *)
18 return 1
19 ;;
20 esac
21 }
22
23 # $1: local file path
24 # $2: remote file path (optional)
25 bsttool_replace ()
26 {
27 if [ $# -lt 1 ]; then
28 echo "Usage: bstput src [dst]"
29 return 1
30 fi
31
32 local src=$1
33 local dst="/"
34 if [ $# -gt 1 ]; then
35 dst=$2
36 fi
37
38 # first check dest existence
39 local resp
40 local name=${src##*/} # remove path part, want name only
41 if [ -z "$name" ]; then
42 name="$src"
43 fi
44
45 local path=$dst/$name
46 bsttool_query_path "$dst"
47 local res=$?
48 case $res in
49 0)
50 # not exist
51 echo "dir/file not exist, start uploading"
52 ;;
53 1)
54 # file exist
55 bsttool_duplicate_warning "$dst"
56 if [ $? -ne 0 ]; then
57 return 1
58 fi
59 ;;
60 2)
61 # empty dir exist
62 echo "dir exist but empty, start uploading"
63 ;;
64 3)
65 # dir exist and not empty
66 # try file again
67 echo "dir exist, try file"
68 bsttool_query_path "$path"
69 res=$?
70 if [ $res -eq 1 ]; then
71 # dest file exist, warning...
72 bsttool_duplicate_warning "$path"
73 if [ $? -ne 0 ]; then
74 return 1
75 fi
76 fi
77 ;;
78 *)
79 echo "should not reach here !!"
80 return 1
81 ;;
82 esac
83
84 bst_tool put "$src" "$dst"
85 return 0
86 }

下面做個簡單說明:

  • line 1-21:衝突提醒,並獲取使用者輸入,如果使用者確認覆蓋,則在 put 前呼叫 delete 刪除之;
  • line 27-36: 進入正文,檢查並獲取輸入引數;
  • line 38-47: 檢查目標檔案是否存在及屬性;
  • line 49-52: 不存在,可以上傳;
  • line 53-59: 存在,調提醒函式獲取使用者輸入,如果使用者拒絕覆蓋,退出;否則繼續;
  • line 60-63:目錄存在但為空,可以上傳;
  • line 64-69:目錄存在且不為空,繼續判斷子目錄;
  • line 70-76:子目錄中存在同名檔案,調提醒函式獲取使用者輸入,如果使用者拒絕覆蓋,退出;否則繼續;
  • line 84: 如果能走到這裡,說明前面沒有檔名稱衝突、或使用者同意覆蓋檔案且後臺檔案已被清理,執行上傳。

為了簡化呼叫,還可以使用 alias 重新命名上面的 shell function:

# alias
alias bstput=bsttool_replace
alias bstget='bst_tool get'
alias bstdel='bst_tool delete'
alias bstls='bst_tool ls'

將上面的指令碼儲存在 ~/.bash_bst 下並在 shell 配置檔案中寫入這樣一行配置:

source ~/.bash_bst

這樣我就可以在命令列使用 bstxx 系列命令代替笨重的 bst_tool xxx 了 (後者仍可用),而且這套 alias 拓展了原命令的功能,使用 bstput 就可以實現覆蓋上傳啦,下面是執行效果:

$ bstput data /data
dir or file exist, try download to check
dir exist, try file
dir or file exist, try download to check
dest file exist, do you want to cover it? (n/y) y
當前使用者:yunhai
deleting the 1th filePath: /data/data
delete result: 成功
當前使用者:yunhai
uploading... 100.00 %
upload success:
FileId: 419402686
FilePath: /data/data
Time: 2021-05-14 19:49:20

將本地的 data 檔案上傳到後臺 /data,而後臺現在已經有 /data/data 的檔案,所以這裡判斷出來有衝突,提示使用者是否覆蓋,得到授權後,刪除後臺檔案後上傳成功。

整個過程呼叫了兩次 bsttool_query_path,第一次針對 /data (total size == 1),確定它是一個目錄;第二次針對 /data/data (total size == 1),確定它是一個檔案。

覆蓋下載

覆蓋下載就相對簡單多了,因為要判斷是否重複的檔案位於本地,可以動用的手段就豐富了。下面直接上指令碼:

 1 # $1: local path
2 #
3 # return value
4 # 0: user allow and delete existing local file
5 # 1: user disallow and do nothing
6 bsttool_existence_warning()
7 {
8 local resp
9 echo -n "local file exist, do you want to cover it? (n/y) "
10 read resp
11 case "$resp" in
12 "y"|"Y")
13 # remove local reource first
14 rm "$1"
15 return 0
16 ;;
17 *)
18 return 1
19 ;;
20 esac
21 }
22
23
24 # $1: remote file path
25 # $2: local file path (optional)
26 bsttool_fetch ()
27 {
28 if [ $# -lt 1 ]; then
29 echo "Usage: bstget remote [local]"
30 return 1
31 fi
32
33 local remote=$1
34 local name=${remote##*/} # remove path part, want name only
35 if [ -z "$name" ]; then
36 name="$remote"
37 fi
38
39 local local="./"
40 if [ $# -gt 1 ]; then
41 local=$2
42 fi
43
44 # check dest existence
45 if [ -e "$local" ]; then
46 if [ -d "$local" ]; then
47 # for dir, test sub file existence
48 if [ -e "$local/$name" -a ! -d "$local/$name" ]; then
49 # dest file exist, warning...
50 bsttool_existence_warning "$local/$name"
51 if [ $? -ne 0 ]; then
52 return 1
53 fi
54 fi
55 else
56 #elif [ -f "$local" ]; then
57 # dest file exist, warning...
58 bsttool_existence_warning "$local"
59 if [ $? -ne 0 ]; then
60 return 1
61 fi
62 fi
63 fi
64
65 bst_tool get "$remote" "$local"
66 return 0
67 }

下面做個簡單說明:

  • line 6-21:本地檔案存在時輸出的警告資訊,如果使用者同意覆蓋,呼叫 rm 移除本地同名檔案並返回 0,否則返回 1;
  • line 28-42:進入正文,檢查並獲取輸入引數,當用戶未提供本地路徑或提供的本地路徑是個目錄時,需要取遠端檔名作為本地檔名,所以這裡有擷取本地路徑名的操作;
  • line 45:檢查本地檔案是否存在,注意這裡使用 -e 來檢查所有檔案型別;
  • line 46:本地目錄存在,繼續檢查目錄;
  • line 48-54:目錄下有同名檔案存在 (如果是目錄則沒關係,可以共存),調提醒函式獲取使用者輸入,如果使用者拒絕覆蓋,退出;否則繼續;
  • line 57-61:本地檔案存在,調提醒函式獲取使用者輸入,如果使用者拒絕覆蓋,退出;否則繼續;
  • line 65:執行下載,此時不會衝突。

同 bstput,加入以下內容來簡化命令呼叫:

alias bstget=bsttool_fetch

下面是執行效果:

$ bstget /data/data
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/data
$ bstget /data/data data
local file exist, do you want to cover it? (n/y) n
$ bstget /data/data test
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/test/data
$ bstget /data/data test
local file exist, do you want to cover it? (n/y) n
$ bstget /data/data test/data
local file exist, do you want to cover it? (n/y) y
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/test/data

第一次下載時都是沒有衝突的,當再次下載時就會提醒了,可以看到針對檔案、目錄的情況都能正常處理。

遞迴列出

軟柿子捏完了,再來看遞迴列出、遞迴下載。它們能否實現的關鍵就在於能否區分遠端路徑為目錄,因為對目錄需要遞迴呼叫 shell 函式做遍歷。這裡如果使用之前判斷遠端檔案屬性的 bsttool_query_path 函式就有點兒太重了,其實使用 TYPE 欄位就足夠了,因為我們只是將檔案羅列出來就夠了,不需要上傳或下載。那如何獲取檔案的 TYPE 欄位呢?讓我們先來看一下 bst_tool ls 的輸出:

$ bstls /ollvm
當前使用者:yunhai
total size: 12
│──────────────│──────────│──────────│─────────────────────│──────│─────────────────────│
│ FILEID (12) │ USERNAME │ FILESIZE │ TIME │ TYPE │ FILENAME │
│──────────────│──────────│──────────│─────────────────────│──────│─────────────────────│
│ 423324279 │ yunhai │ 289.00B │ 2021-05-24 14:28:33 │ file │ build_glibc.sh │
│ 423324280 │ yunhai │ 15.73MB │ 2021-05-24 14:28:33 │ file │ cmake-3.20.0.zip │
│ 423324281 │ yunhai │ 409.00B │ 2021-05-24 14:28:33 │ file │ build_cmake.sh │
│ 423324283 │ yunhai │ 14.28KB │ 2021-05-24 14:28:33 │ file │ bash_bst.txt │
│ 423324284 │ yunhai │ 820.00B │ 2021-05-24 14:28:33 │ file │ create_user.sh │
│ 423324278 │ yunhai │ 2.85MB │ 2021-05-24 14:28:33 │ file │ global-6.6.5.tar.gz │
│ 423324285 │ yunhai │ 535.00B │ 2021-05-24 14:28:33 │ file │ init_bst.sh │
│ 405005475 │ yunhai │ │ 2021-04-07 20:49:02 │ dir │ tutor │
│ 405004793 │ yunhai │ │ 2021-04-07 20:41:01 │ dir │ 81 │
│ 404965309 │ yunhai │ │ 2021-04-07 18:19:08 │ dir │ 8 │
│ 404943904 │ yunhai │ │ 2021-04-07 17:46:10 │ dir │ 4 │
│ 404938470 │ yunhai │ │ 2021-04-07 17:37:02 │ dir │ common │
│──────────────│──────────│──────────│─────────────────────│──────│─────────────────────│

可以看到目錄的 TYPE 為 dir,普通檔案為 file;除了這兩個欄位,為了給使用者列出檔案詳情,我們還需要獲取 FILENAME / FILESIZE / FILEID 三個欄位,為此最好是一次性從輸出中獲取它們,以分隔符劃分並獲取各個欄位的辦法,我第一個想到的就是 awk:

$ bstls /ollvm | sed -n '6p' | awk -F'│' '{printf ("%s\t%s\t%s\t%s\n", $2, $4, $6, $7)}'
423324279 289.00B file build_glibc.sh

這段指令碼先提取輸出中的一行 (sed -n '6p'),這裡資料從第6行開始,所以必需大於等於6;然後使用豎線分隔該行各個欄位並通過 $n 列印需要的欄位,注意這裡的豎線不是普通的 ‘|’,而是更大更長的豎線,這個我真不知道怎麼從鍵盤上敲出來,最後還是從 bst 的輸出中複製的才搞定。由於我們只關心第 1 / 3 / 5 / 6 列,所以下標選擇了 2 / 4 / 6 / 7,這是因為分隔後下標為 1 的第一列對應是個空列,需要跳過。如果我們用這個指令碼跑一下目錄,它能否正確輸出呢?

$ bstls /ollvm | sed -n '14p' | awk -F'│' '{printf ("%s\t%s\t%s\t%s\n", $2, $4, $6, $7)}'
405004793 dir 81

第一個目錄位於 14 行,所以需要更新 sed 引數為 14p,後面的不變。可以看到由於目錄沒有 FILESIZE 欄位,導致輸出後只有三項,這樣一來當我們繼續提出的時候,就會少了一列,和上面檔案的格式不統一了,有什麼辦法可以為空欄位補零的嗎?答案是使用 awk 的條件判斷語句:

$ bstls /ollvm | sed -n '14p' | awk -F'│' '{printf ("%s\t%s\t%s\t%s\n", $2, match($4, /[a-zA-Z0-9.]+/)?$4:"0", $6, $7)}'
405004793 0 dir 81

awk 中的可以用 match 表示式來進行正則匹配,如果第 4 列滿足檔案大小的正則表示式 ([a-zA-Z0-9.]+),那麼就使用對應的欄位,否則使用 0 代替。可以看到新的輸出中包含了4 個欄位,第 2 個欄位正確的補零了。ok,有了這個基礎,再怎麼將它們賦值給 shell 的變數呢?最簡單的辦法,還是使用 awk,將想要賦值的欄位 print 出來,類似這樣:

$ filesize=`bstls /ollvm | sed -n '7p' | awk -F'│' '{printf ("%s\t%s\t%s\t%s\n", $2, match($4, /[a-zA-Z0-9.]+/)?$4:"0", $6, $7)}' | awk '{printf $2}'`
$ echo $filesize
15.73MB

這次以第 7 行的檔案資料為例,將之前的 awk 輸出再經過 awk 過濾一次,得到的值賦值給 shell 變數,再列印變數即可,可以看到打印出的結果是符合預期的。由於這裡使用的是預設的空格和 TAB 鍵分隔,所以不需要特別指定 awk 的分隔符,從這裡也可以看出來上面對目錄大小為空的處理是必要的,不然空列會直接被忽略,後面的欄位就對不上了。但是這樣一個一個  print 的缺點是效率太低了,提取 4 個欄位就需要執行 4 次賦值,有沒有辦法一次提取 4 個欄位呢?答案就是使用 eval:

$ eval `bstls /ollvm | sed -n '7p' | awk -F'│' '{printf ("%s\t%s\t%s\t%s\n", $2, match($4, /[a-zA-Z0-9.]+/)?$4:"0", $6, $7)}' | awk '{printf ("fid=%s;size=%s;type=%s;name=%s;\n", $1,$2,$3,$4)}'`
$ echo -e "fid=$fid\tsize=$size\ttype=$type\tname=$name"
fid=423324280 size=15.73MB type=file name=cmake-3.20.0.zip

eval 命令接收一個字串,並將這個串按 shell 語法去解釋並應用於當前 shell;所以問題的關鍵變成如何構造一個 shell 語句來實現多個欄位的同時賦值,於是就有了後面這個 awk 語句:

awk '{printf ("fid=%s;size=%s;type=%s;name=%s;\n", $1,$2,$3,$4)}'

它將我們之前得到的輸出以 shell 變數賦值的方式輸出出來,再交給 eval 去‘評估’,這樣就完成了整個賦值過程,於是我們看到,在後面 echo 語句中列印這些變數時,得到了正確的輸出。 ok,有了這些做鋪墊就可以正式亮出遞迴列出的程式碼了:

 1 # $1: remote path
2 # $2: depth
3 bsttool_list_recur ()
4 {
5 local remote=$1
6 local depth=$(($2+1))
7 local res=$(bst_tool ls "$remote")
8 local line=$(echo "$res" | sed -n '2p')
9 local str=${line/"total size"//}
10 local ret=0
11 if [ "$str" != "$line" ]; then
12 # normal return
13 ret=$(echo "$line" | awk '{print $NF}')
14 else
15 # server response error
16 ret=0
17 fi
18
19 # 6: skip header
20 local max=$(($ret + 6 - 1))
21 local fid=""
22 local size="" # has K/M/G postfix
23 local type=""
24 local name=""
25 echo "$remote [$depth-$ret]:"
26 for n in $(seq 6 $max)
27 do
28 # why sed -n "$np" don't work ?
29 line=$(echo "$res" | sed -n "$n"'p')
30 # row: fid, user, size, timestamp, type, name
31 # if size is empty (for dir, all spaces), using 0 instead
32 # do awk twice to remove redundant space
33 #
34 # add quotation for assignment is important, otherwise we will get errors on bracket (etc) in value
35 eval $(echo "$line" | awk -F'│' '{printf ("%s\t%s\t%s\t%s\n", $2, match($4, /[a-zA-Z0-9.]+/)?$4:"0", $6, $7)}' | awk '{printf ("fid=\"%s\";size=\"%s\";type=\"%s\";name=\"%s\";\n", $1,$2,$3,$4)}')
36 if [ "$type" == "dir" ]; then
37 # do for sub dirs
38 local path=""
39 if [ ${remote:0-1} == "/" ]; then
40 path="$remote$name"
41 else
42 path="$remote/$name"
43 fi
44
45 bsttool_list_recur "$path" $depth
46 else
47 echo -e "$fid:\t$size,\t$name"
48 fi
49 done
50
51 return 0
52 }
53
54 # $1: remote path
55 bsttool_list_all ()
56 {
57 local remote="/"
58 if [ $# -gt 0 ]; then
59 remote=$1
60 fi
61
62 bsttool_list_recur "$remote" 0
63 }

下面做個簡單說明:

  • line 7:呼叫 bst_tool ls 列出遠端根目錄;
  • line 8-17:獲取輸出檔案項數量,如果後臺應答有錯誤,則按沒有輸出處理;
  • line 20-26:宣告變數,跳過表頭,準備遍歷檔案項;
  • line 29:擷取某一行檔案資料;
  • line 35:獲取該行中各個欄位並賦值給變數;
  • line 36-45:對於目錄項,遞迴列出其中檔案;
  • line 47:對於檔案項,直接列出詳情;
  • line 55-63:處理初始情況 (確定根目錄、給定深度),開始遞迴。

同上,加入以下內容來簡化命令呼叫:

alias bstrls=bsttool_list_all

在 bst 和 ls 之間有一個 'r' 字元,表示遞迴。下面是執行效果:

$ bstrls /ollvm
/ollvm [1-12]:
423324279: 289.00B, build_glibc.sh
423324280: 15.73MB, cmake-3.20.0.zip
423324281: 409.00B, build_cmake.sh
423324283: 14.28KB, bash_bst.txt
423324284: 820.00B, create_user.sh
423324278: 2.85MB, global-6.6.5.tar.gz
423324285: 535.00B, init_bst.sh
/ollvm/tutor [2-5]:
405006643: 1.32KB, build_ollvm.sh
405005476: 819.00B, build_skeleton.sh
404994390: 310.00B, test.c
403551141: 248.28KB, llvm-pass-tutorial-dev.zip
403531758: 24.79KB, llvm-pass-tutorial.tar.gz
/ollvm/81 [2-5]:
405073802: 2.38KB, copy_ollvm81.sh
405004794: 1.81KB, build_llvm81.sh
404314927: 44.50KB, PassManagerBuilder.cpp
403560689: 12.22MB, cfe-8.0.1.src.tar.xz
403560381: 29.07MB, llvm-8.0.1.src.tar.xz
/ollvm/8 [2-3]:
406677215: 1.66KB, copy_ollvm8.sh
406677122: 531.00B, build_ollvm8.sh
403203783: 83.16MB, obfuscator-llvm-8.0.zip
/ollvm/4 [2-3]:
404951254: 1.30KB, copy_ollvm4.sh
404943908: 320.00B, build_ollvm4.sh
402300052: 180.29MB, obfuscator.tar.gz
/ollvm/common [2-7]:
406687742: 3.45KB, compare_instructions.sh
406687716: 1.21KB, inject_so.sh
406678282: 899.00B, clone_netdisk.sh
406678068: 401.00B, download_ndk.sh
404942152: 181.00B, build_p2psrc.sh
404940645: 748.00B, clone_p2psrc.sh
402360890: 819.91MB, ndkr20.zip

對於目錄,沒有列出 FILEID 和 FILESIZE 欄位,代之以目錄深度和檔案數量 ([x-x]);檔案項的話個人覺得反而比原版命令清爽一些,嗯就這樣了。使用 bstrls + grep,查詢一個檔案在不在後臺就變得容易多了,不過總體執行速度堪憂,考慮到一個命令下去,底層執行了 N 多次 bst_tool 命令,情有可原~

遞迴下載

剩下最後一根硬骨頭了,不過有了遞迴列出的基礎,遞迴下載也沒那麼難了。與覆蓋下載遇到相同的問題是需要提前判斷檔案是否已經在本地存在,防止意外覆蓋資料;不同的點是,遞迴下載可能會有多次覆蓋提醒,如果每次都要讓使用者選擇,那也不是不行,畢竟有 yes 這種工具,不過這種工具是提前設定好了 yes 或 no 的選項,沒有辦法隨機應變,想要靈活性與便利性都具備,最好還是自己處理選項,最簡單的辦法是用大寫字母表示應用全部,小寫表示只應用一次:

  • y:允許一次
  • Y:全部允許
  • n:禁止一次
  • N:全部禁止

那麼這個選項變數能作為 shell 函式的引數傳遞嗎?答案是不能,因為雖然可以向下傳遞,但是如果使用者在深層呼叫中改變了選擇,這個變化卻不能向上傳遞,畢竟 shell 函式引數只能作為輸入引數,不能像 c / c++ / java 那種高階程式設計語言一樣可以有輸出引數,如果使用 shell 函式的 return 語句作為輸出,那麼它本身的返回碼又不能用了。綜合考慮,最後使用者輸入的選項作為全域性變數,在遞迴開始前設定為預設值,在遞迴過程中改變,以便影響後續的判斷過程。

另外一個不同點是,隨著目錄的深入、函式的遞迴,需要維護好當前工作目錄,保證每次建立檔案時所處的目錄是正確的,當目錄遍歷結束返回上層時 (或函式遞迴呼叫結束返回呼叫點),需要返回上級目錄。對於遍歷過程,直接 cd .. 就可以了,但是對於首次進入遞迴,需要儲存當前工作目錄 (PWD),因為下載目錄可能是個多級目錄,一次 cd .. 是回不來的。

  1 # global variable
2 # we can NOT pass these as recursive function parameters,
3 # as shell function can NOT pass modification back to callee.
4 #
5 # cover flag (1:no once 2:no all 3:yes once 4:yes all)
6 g_cover_flag=1
7
8 # return value
9 # 0: invalid input
10 # 1: no once
11 # 2: no all
12 # 3: yes once
13 # 4: yes all
14 bsttool_duplicate_dir_warning()
15 {
16 local resp
17 echo -n "dest file/dir exist, do you want to cover it? (n/N/y/Y) "
18 read resp
19 case "$resp" in
20 "n")
21 return 1
22 ;;
23 "N")
24 return 2
25 ;;
26 "y")
27 return 3
28 ;;
29 "Y")
30 return 4
31 ;;
32 *)
33 return 0
34 ;;
35 esac
36 }
37
38 # $1: remote path
39 # $2: local path (optional)
40 bsttool_get_all ()
41 {
42 if [ $# -lt 1 ]; then
43 echo "Usage: bstrget remote [local]"
44 return 1
45 fi
46
47 local remote=$1
48 local local=""
49 local type=0
50 g_cover_flag=1 # no once
51 local dirold=""
52 if [ $# -gt 1 ]; then
53 local=$2
54 else
55 # get name part
56 local tmp=""
57 if [ ${remote:0-1} == "/" ]; then
58 # cut tailing '/'
59 tmp="${remote:0:$((${#remote}-1))}"
60 else
61 tmp="$remote"
62 fi
63
64 local="${tmp##*/}"
65 fi
66
67 if [ -z "$local" -o "$local" == "/" ]; then
68 # default name
69 local="setup"
70 fi
71
72 bsttool_query_path "$remote"
73 type=$?
74 case $type in
75 0)
76 # not exist
77 echo "remote not exist"
78 return 1
79 ;;
80 1)
81 # file exist
82 # do nothing, leave file to bsttool_get_recur
83 ;;
84 2)
85 # empty dir exist
86 echo "nothing to download"
87 return 1
88 ;;
89 3)
90 # dir exist
91 if [ ! -d "$local" ]; then
92 mkdir -p "$local"
93 else
94 bsttool_duplicate_dir_warning
95 g_cover_flag=$?
96 # invalid or no (all)
97 if [ $g_cover_flag -eq 0 -o $g_cover_flag -eq 1 -o $g_cover_flag -eq 2 ]; then
98 return 1
99 fi
100 fi
101
102 dirold="$PWD"
103 cd "$local"
104 ;;
105 *)
106 echo "should not reach here !"
107 return 1
108 ;;
109 esac
110 111 bsttool_get_recur "$remote" "$local" 0
112 if [ ! -z "$dirold" ]; then
113 cd "$dirold"
114 fi
115 return 0
116 }

下面做個簡單說明:

  • line 6:全域性使用者覆蓋選項變數;
  • line 14-36:用於獲取使用者選擇的提醒函式;
  • line 42-70:進入正文,獲取並檢查輸入引數,如果沒有提供本地下載路徑,預設為當前目錄;檔名為遠端路徑的檔名部分,如果遠端路徑為根目錄,本地下載目錄預設為 setup;
  • line 72-73:查詢遠端路徑屬性,bsttool_query_path 在之前的覆蓋上傳中有過介紹;
  • line 75-79:遠端路徑不存在,直接退出,退出碼為 1;
  • line 80-83:遠端檔案存在,什麼也不做,留給後面的遞迴函式處理;
  • line 84-88:遠端目錄為空,直接退出,退出碼為 1;
  • line 89-104:遠端目錄存在且不為空,如果本地路徑不存在,遞迴建立之;否則提示使用者是否覆蓋該目錄,如果使用者選擇否,直接退出,退出碼為 1。如果使用者選擇覆蓋或目錄是新建立的,則記錄舊目錄,切換到下載目錄,準備開始遞迴下載;
  • line 105-108:後臺出錯,直接退出,退出碼為 1;
  • line 111:啟動遞迴下載;
  • line 112-114:恢復初始工作目錄。

重頭戲都放在了 bsttool_get_recur 中:

 1 # $1: remote path
2 # $2: local path
3 # $3: depth
4 bsttool_get_recur ()
5 {
6 local remote=$1
7 local local=$2
8 local depth=$(($3+1))
9
10 local res=$(bst_tool ls "$remote")
11 local line=$(echo "$res" | sed -n '2p')
12 local str=${line/"total size"//}
13 local ret=0
14 if [ "$str" != "$line" ]; then
15 # normal return
16 ret=$(echo "$line" | awk '{print $NF}')
17 else
18 # server response error
19 ret=0
20 fi
21
22 # 6: skip header
23 local max=$(($ret + 6 - 1))
24 local fid=""
25 local size="" # has K/M/G postfix
26 local type=""
27 local name=""
28 local path=""
29 local filesize=0
30 echo "$remote [$depth-$ret]:"
31 for n in $(seq 6 $max)
32 do
33 # why sed -n "$np" don't work ?
34 line=$(echo "$res" | sed -n "$n"'p')
35 # row: fid, user, size, timestamp, type, name
36 # if size is empty (for dir, all spaces), using 0 instead
37 # do awk twice to remove redundant space
38 eval $(echo "$line" | awk -F'│' '{printf ("%s\t%s\t%s\t%s\n", $2, match($4, /[a-zA-Z0-9.]+/)?$4:"0", $6, $7)}' | awk '{printf ("fid=%s;size=%s;type=%s;name=%s;\n", $1,$2,$3,$4)}')
39 if [ "$type" == "dir" ]; then
40 if [ -d "$name" ]; then
41 echo "dir exist: $name"
42 if [ $g_cover_flag -eq 1 -o $g_cover_flag -eq 3 ]; then
43 bsttool_duplicate_dir_warning
44 g_cover_flag=$?
45 fi
46
47 # do NOT rm dirs even if user select cover it !
48 # just merge files into it ..
49 #
50 # if not yes once or all, then nothing to do
51 if [ $g_cover_flag -ne 3 -a $g_cover_flag -ne 4 ]; then
52 echo "nothing to do"
53 continue
54 fi
55 else
56 mkdir "$name"
57 fi
58
59 cd "$name"
60 # do for sub dirs
61 if [ ${remote:0-1} == "/" ]; then
62 path="$remote$name"
63 else
64 path="$remote/$name"
65 fi
66
67 bsttool_get_recur "$path" "$name" $depth
68 cd ..
69 else
70 echo -e "$fid:\t$size,\t$name"
71 if [ -f "$name" ]; then
72 echo "file exist: $name"
73 if [ $g_cover_flag -eq 1 -o $g_cover_flag -eq 3 ]; then
74 bsttool_duplicate_dir_warning
75 g_cover_flag=$?
76 fi
77
78 if [ $g_cover_flag -eq 3 -o $g_cover_flag -eq 4 ]; then
79 echo "remove old file before download"
80 rm "$name"
81 else
82 # no need to download
83 echo "nothing to do"
84 continue
85 fi
86 fi
87
88 if [ ${remote:0-1} == "/" ]; then
89 path="$remote$name"
90 else
91 path="$remote/$name"
92 fi
93
94 bst_tool get "$path" "$name"
95 fi
96 done
97
98 return 0
99 }

前面獲取資訊這部分和遞迴列出非常類似,就不贅述了,下面主要對下載這一部分做個簡單說明 (line 39+):

  • line 39-54:如果是目錄,且本地已存在,且使用者之前未選擇全部應用 (no once 或 yes once),則提醒使用者是否覆蓋,如果使用者選擇了不覆蓋,或者之前使用者選擇的是 no all,則跳過該目錄,繼續處理下個檔案項;
  • line 56:如果目錄不存在,則建立之;
  • line 59-68:切換到新目錄,構建新的路徑,呼叫自身遞迴處理目錄中的檔案項,處理完畢後回退到上級目錄;
  • line 70-86:輸出下載檔案的詳細資訊,如果檔案已存在,且使用者之前未選擇全部應用 (no once 或 yes once),則提示使用者是否覆蓋檔案,如果使用者選擇了不覆蓋,或者之前使用者選擇的是 no all,則跳過該檔案,繼續處理下個檔案項。否則刪除本地同名檔案,防止之後下載時產生衝突;
  • line 88-94:構建下載檔案路徑,啟動 bst_tool 下載檔案。

在使用這個工具做檔案備份的時候,我發現一個新的需求點,就是我只希望備份自己寫的指令碼檔案,一些安裝包、壓縮包等較大的檔案可以從網上下載,沒必要備份,但是這個工具一次性全下載下來了,既佔用空間,又浪費頻寬。自然而然想到的解決方案就是通過檔案尺寸來過濾下載項,只有小於閾值的檔案才被下載,和覆蓋選項一樣,我們希望它能在靈活性和便利性上能達到兼顧,於是依葫蘆畫瓢,再整一個下載限制的選項:

 1 # global variable
2 # we can NOT pass these as recursive function parameters,
3 # as shell function can NOT pass modification back to callee.
4 #
5 # cover flag (1:no once 2:no all 3:yes once 4:yes all)
6 g_cover_flag=1
7 # size limit flag (1:no once 2:no all 3:yes once 4:yes all)
8 g_limit_flag=1
9 g_size_limit=1 # MB
10
11 # return value
12 # 0: invalid input
13 # 1: no once
14 # 2: no all
15 # 3: yes once
16 # 4: yes all
17 bsttool_huge_file_warning()
18 {
19 local resp
20 echo -n "do you want to continue download this large file? (n/N/y/Y) "
21 read resp
22 case "$resp" in
23 "n")
24 return 1
25 ;;
26 "N")
27 return 2
28 ;;
29 "y")
30 return 3
31 ;;
32 "Y")
33 return 4
34 ;;
35 *)
36 return 0
37 ;;
38 esac
39 }
40
41 # $1: remote path
42 # $2: local path (optional)
43 bsttool_get_all ()
44 {
45 ……
46 g_limit_flag=1 # no once
47 g_size_limit=$((1024*1024)) # in bytes
48 bsttool_get_recur "$remote" "$local" 0
49 if [ ! -z "$dirold" ]; then
50 cd "$dirold"
51 fi
52 }

它由兩個選項組成,其中 g_limit_flag 表示使用者的選擇,通過呼叫 bsttool_huge_file_warning 詢問使用者獲取,後者和覆蓋選項幾乎完全相同; g_size_limit 表示檔案尺寸閾值,這個目前固定為 1 MB,後續可更改為通過環境變數設定。有了變數的定義和初值,就可以在遞迴函式中進行判斷了:

 1             echo -e "$fid:\t$size,\t$name"
2 # contains KB?
3 local kfactor=${size/"KB"//}
4 if [ "$kfactor" != "$size" ]; then
5 kfactor=1024
6 else
7 kfactor=1
8 fi
9
10 # contains MB?
11 local mfactor=${size/"MB"//}
12 if [ "$mfactor" != "$size" ]; then
13 mfactor=$((1024*1024))
14 else
15 mfactor=1
16 fi
17
18 # contains GB?
19 local gfactor=${size/"GB"//}
20 if [ "$gfactor" != "$size" ]; then
21 gfactor=$((1024*1024*1024))
22 else
23 gfactor=1
24 fi
25
26 # should we support TB?
27 filesize=$(awk -v val="$size" -v k="$kfactor" -v m="$mfactor" -v g="$gfactor" 'BEGIN{ printf ("%d", strtonum(val)*k*m*g) }')
28 if [ $filesize -gt $g_size_limit ]; then
29 echo "file too huge: $g_limit_flag"
30 if [ $g_limit_flag -eq 1 -o $g_limit_flag -eq 3 ]; then
31 bsttool_huge_file_warning
32 g_limit_flag=$?
33 fi
34
35 # if not yes once or all, then nothing to do
36 if [ $g_limit_flag -ne 3 -a $g_limit_flag -ne 4 ]; then
37 # no need to download
38 echo "nothing to do"
39 continue
40 fi
41 fi

下面做個簡單說明:

  • line 1:當下載的檔案型別為普通檔案時,已經提取到了檔案的尺寸資訊,不過這個資訊是以各種不同單位結尾的字串,單位有 KB/MB/GB;
  • line 2-27:將它們統一轉換為位元組為單位,這裡使用 awk strtonum 來將字串轉為 double 精度數字,strtonum 會自動忽略不能轉換為數字的單位部分。另外通過檢查有無單位關鍵字來確定使用的乘積因子,最後使用 awk 的乘法來獲取最終以位元組為單位 double 精度的乘積結果。這裡有幾點需要注意:
    • 使用 strtonum 將字串轉換為數字;
    • 向 awk 傳遞 shell 變數 (-v);
    • awk 的乘除預設是 double 精度的,反而想要整數乘除結果會比較費勁;
  • line 28-41:當檔案尺寸換算為位元組數超過閾值時,且使用者之前未選擇全部應用 (no once 或 yes once),則提示使用者是否下載大檔案,如果使用者選擇了不下載,或者之前使用者選擇的是 no all,則跳過該檔案,繼續處理下個檔案項。

同上,加入以下內容來簡化命令呼叫:

alias bstrget=bsttool_get_all

ok,至此一個比較實用的備份指令碼工具做好了,我們用它來備份一下剛才的目錄:

$ bstrget /ollvm
/ollvm [1-12]:
423324279: 289.00B, build_glibc.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/build_glibc.sh
423324280: 15.73MB, cmake-3.20.0.zip
file too huge: 1
do you want to continue download this large file? (n/N/y/Y) n
nothing to do
423324281: 409.00B, build_cmake.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/build_cmake.sh
423324283: 14.28KB, bash_bst.txt
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/bash_bst.txt
423324284: 820.00B, create_user.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/create_user.sh
423324278: 2.85MB, global-6.6.5.tar.gz
file too huge: 1
do you want to continue download this large file? (n/N/y/Y) y
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/global-6.6.5.tar.gz
423324285: 535.00B, init_bst.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/init_bst.sh
/ollvm/tutor [2-5]:
405006643: 1.32KB, build_ollvm.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/tutor/build_ollvm.sh
405005476: 819.00B, build_skeleton.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/tutor/build_skeleton.sh
404994390: 310.00B, test.c
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/tutor/test.c
403551141: 248.28KB, llvm-pass-tutorial-dev.zip
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/tutor/llvm-pass-tutorial-dev.zip
403531758: 24.79KB, llvm-pass-tutorial.tar.gz
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/tutor/llvm-pass-tutorial.tar.gz
/ollvm/81 [2-5]:
405073802: 2.38KB, copy_ollvm81.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/81/copy_ollvm81.sh
405004794: 1.81KB, build_llvm81.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/81/build_llvm81.sh
404314927: 44.50KB, PassManagerBuilder.cpp
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/81/PassManagerBuilder.cpp
403560689: 12.22MB, cfe-8.0.1.src.tar.xz
file too huge: 3
do you want to continue download this large file? (n/N/y/Y) N
nothing to do
403560381: 29.07MB, llvm-8.0.1.src.tar.xz
file too huge: 2
nothing to do
/ollvm/8 [2-3]:
406677215: 1.66KB, copy_ollvm8.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/8/copy_ollvm8.sh
406677122: 531.00B, build_ollvm8.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/8/build_ollvm8.sh
403203783: 83.16MB, obfuscator-llvm-8.0.zip
file too huge: 2
nothing to do
/ollvm/4 [2-3]:
404951254: 1.30KB, copy_ollvm4.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/4/copy_ollvm4.sh
404943908: 320.00B, build_ollvm4.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/4/build_ollvm4.sh
402300052: 180.29MB, obfuscator.tar.gz
file too huge: 2
nothing to do
/ollvm/common [2-7]:
406687742: 3.45KB, compare_instructions.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/common/compare_instructions.sh
406687716: 1.21KB, inject_so.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/common/inject_so.sh
406678282: 899.00B, clone_netdisk.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/common/clone_netdisk.sh
406678068: 401.00B, download_ndk.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/common/download_ndk.sh
404942152: 181.00B, build_p2psrc.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/common/build_p2psrc.sh
404940645: 748.00B, clone_p2psrc.sh
當前使用者:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/common/clone_p2psrc.sh
402360890: 819.91MB, ndkr20.zip
file too huge: 2
nothing to do

輸出中高亮的部分表示我實際輸入的選擇,第一個 15 MB 多的檔案 no once;第二個 3 MB 多的檔案 yes once;第三個 12 MB 多的檔案 no all。之後凡是大於 1 MB 的檔案就自動跳過了,可以看到 file too huge 輸出的就是。檢查下載目錄後,確實如此:

$ ls -lhR ollvm
ollvm:
total 3.0M
drwxrwxr-x 2 yunh yunh 4.0K May 24 20:49 4
drwxrwxr-x 2 yunh yunh 4.0K May 24 20:49 8
drwxrwxr-x 2 yunh yunh 4.0K May 24 20:49 81
-rw-rw-r-- 1 yunh yunh 15K May 24 20:49 bash_bst.txt
-rw-rw-r-- 1 yunh yunh 409 May 24 20:49 build_cmake.sh
-rw-rw-r-- 1 yunh yunh 289 May 24 20:48 build_glibc.sh
drwxrwxr-x 2 yunh yunh 4.0K May 24 20:49 common
-rw-rw-r-- 1 yunh yunh 820 May 24 20:49 create_user.sh
-rw-rw-r-- 1 yunh yunh 2.9M May 24 20:49 global-6.6.5.tar.gz
-rw-rw-r-- 1 yunh yunh 535 May 24 20:49 init_bst.sh
drwxrwxr-x 2 yunh yunh 4.0K May 24 20:49 tutor ollvm/4:
total 8.0K
-rw-rw-r-- 1 yunh yunh 320 May 24 20:49 build_ollvm4.sh
-rw-rw-r-- 1 yunh yunh 1.4K May 24 20:49 copy_ollvm4.sh ollvm/8:
total 8.0K
-rw-rw-r-- 1 yunh yunh 531 May 24 20:49 build_ollvm8.sh
-rw-rw-r-- 1 yunh yunh 1.7K May 24 20:49 copy_ollvm8.sh ollvm/81:
total 56K
-rw-rw-r-- 1 yunh yunh 1.9K May 24 20:49 build_llvm81.sh
-rw-rw-r-- 1 yunh yunh 2.4K May 24 20:49 copy_ollvm81.sh
-rw-rw-r-- 1 yunh yunh 45K May 24 20:49 PassManagerBuilder.cpp ollvm/common:
total 24K
-rw-rw-r-- 1 yunh yunh 181 May 24 20:49 build_p2psrc.sh
-rw-rw-r-- 1 yunh yunh 899 May 24 20:49 clone_netdisk.sh
-rw-rw-r-- 1 yunh yunh 748 May 24 20:49 clone_p2psrc.sh
-rw-rw-r-- 1 yunh yunh 3.5K May 24 20:49 compare_instructions.sh
-rw-rw-r-- 1 yunh yunh 401 May 24 20:49 download_ndk.sh
-rw-rw-r-- 1 yunh yunh 1.3K May 24 20:49 inject_so.sh ollvm/tutor:
total 292K
-rw-rw-r-- 1 yunh yunh 1.4K May 24 20:49 build_ollvm.sh
-rw-rw-r-- 1 yunh yunh 819 May 24 20:49 build_skeleton.sh
-rw-rw-r-- 1 yunh yunh 249K May 24 20:49 llvm-pass-tutorial-dev.zip
-rw-rw-r-- 1 yunh yunh 25K May 24 20:49 llvm-pass-tutorial.tar.gz
-rw-rw-r-- 1 yunh yunh 310 May 24 20:49 test.c

perfect! 如果我重新執行一遍上面的命令,還可以看到覆蓋選項的使用,這裡出於篇幅考慮就不再羅列了。

後記

其實還可以實現目錄的遞迴上傳功能,技術上不存在任何障礙,只是對我來說意義不大,就沒有做。

在測試的過程中, 我還發現一個指令碼的 bug,就是當目錄中包含兩個同名檔案時 (一個是普通檔案,一個是目錄),則在 bst_tool ls name 時,將優先輸出目錄的內容,和目錄是否為空、目錄和檔案的建立先後順序都無關。那麼在之前 bsttool_query_path 中對遠端路徑進行判斷時,就有可能出問題 (將上傳的目標檔案理解為目錄)。例如有下面的檔案結構:

$ bst_tool ls /tmp/
當前使用者:yunhai
total size: 2
│───────────│──────────│──────────│─────────────────────│──────│──────────│
│ FILEID │ USERNAME │ FILESIZE │ TIME │ TYPE │ FILENAME │
│───────────│──────────│──────────│─────────────────────│──────│──────────│
│ 424833297 │ yunhai │ │ 2021-05-26 19:45:06 │ dir │ data │
│ 424829696 │ yunhai │ 16.11KB │ 2021-05-26 19:41:06 │ file │ data │
│───────────│──────────│──────────│─────────────────────│──────│──────────│

當我想將檔案上傳到 /tmp 並且命名為 data 時,我會發現實際上傳的路徑是 /tmp/data/xxx 而不是覆蓋 /tmp/data:

$ bstput foo /tmp/data
dir exist but empty, start uploading
當前使用者:yunhai
uploading... 100.00 %
upload success:
FileId: 424841196
FilePath: /tmp/data/foo
Time: 2021-05-26 19:55:53

原因就是我上面說的。這個問題對 bst_tool 也存在,使用同樣的引數呼叫 bst_tool put 會得到下面的輸出:

$ bst_tool put foo /tmp/data
當前使用者:yunhai
uploading... 100.00 %
upload success:
FileId: 424842183
FilePath: /tmp/data/foo(1)
Time: 2021-05-26 19:57:07

可見是上傳到了同一個位置。那就沒有辦法上傳到 /tmp/data 了嗎? 如果將上傳檔案提前重新命名為目標檔名,再將遠端路徑改為檔案的直屬目錄路徑,是不是就可以了呢?

$ mv foo data
$ bstput data /tmp
dir exist, try file
當前使用者:yunhai
uploading... 100.00 %
upload success:
FileId: 424844325
FilePath: /tmp/data(1)
Time: 2021-05-26 20:00:12

結果是令人失望的,本來期望 bstput 會提醒我們有同名檔案衝突是否覆蓋呢,結果直接上傳並重命名了,這就是我開頭說的 bug。讓我們來分析一下為什麼是這個樣子:

  • 當 bsttool_query_path 以 /tmp 為引數進行檢查時,發現它是一個非空目錄;
  • 繼續檢查它下面是不是有名叫 /tmp/data 的檔案,之前我們說過,bst_tool ls /tmp/data 時優先列出目錄內容,於是我們認為這裡有一個同名目錄;
  • 由於檔案可以和目錄同名,於是我們認為不影響,就繼續呼叫 bst_tool put 去做上傳;
  • 而實際上傳後發現已經有這樣一個檔案了,於是將上傳檔案改名。

可以看出來問題的關鍵就是,當 bst_tool ls xxx 告訴你這是一個目錄時,有可能是不成立的。此時還可能存在一個同名的檔案,從而引發上傳衝突。那這個問題怎麼解決呢?好辦,其實還是用之前 ls 解決不了時上 get 的套路,如果後臺僅有一個目錄,此時 bst_tool get xxx 會報錯,否則那個同名檔案會被下載下來,這樣就能知道有沒有同名檔案了。下面是補丁程式碼:

 1         # normal return
2 ret=$(echo "$line" | awk '{print $NF}')
3 if [ $ret -eq 0 ]; then
4 ret=2
5 elif [ $ret -gt 1 ]; then
6 # a dir
7 #ret=3
8 # at this point, we are NOT sure there is no file with same name exist,
9 # as bst_tool ls list directory with preference than file,
10 # so here we need a download try...
11 echo "dir exist, try download to check existence of file with same name"
12 rm "$download" > /dev/null 2>&1 # otherwise bst_tool will fail
13 line=$(bst_tool get "$remote" "$download" | sed -n '2p')
14 str=${line/"download success"//}
15 if [ "$str" != "$line" ]; then # string replace changed: download success
16 # has same name file
17 ret=1
18 else
19 # only dir
20 ret=3
21 fi
22 else # size=1
23 # note here we can NOT using type file/dir to determine,
24 # for example, there is a path /tmp/tmp pointed to a file
25 # when we ls /tmp, it report one item named 'tmp' with type file,
26 # but actually it is a dir.
27 # so here we use another way to determine, that is, download it,
28 # you can NOT download a dir.
29 echo "dir or file exist, try download to check"
30 rm "$download" > /dev/null 2>&1 # otherwise bst_tool will fail
31 line=$(bst_tool get "$remote" "$download" | sed -n '2p')
32 str=${line/"download success"//}
33 if [ "$str" != "$line" ]; then # string replace changed: download success
34 # is a file
35 ret=1
36 else
37 # is a dir
38 ret=3
39 fi
40 fi

原來的程式碼就是 line 7 了,當 bst_tool ls 返回 size 大於 1 後直接判斷為目錄 (3) 返回; 現在加了 line 11-21,用於繼續判斷有無同名檔案。可以看到補丁程式碼和已有的 line 29-39 程式碼非常類似,所以也可以將它們合併在一起,最終得到:

 1         # normal return
2 ret=$(echo "$line" | awk '{print $NF}')
3 if [ $ret -eq 0 ]; then
4 ret=2
5 elif [ $ret -gt 1 ]; then
6 # a dir
7 ret=3
8 # at this point, we are NOT sure there is no file with same name exist,
9 # as bst_tool ls list directory with preference than file,
10 # so here we need a download try...
11 echo "dir exist, try download to check existence of file with same name"
12 else # size=1
13 ret=3
14 # note here we can NOT using type file/dir to determine,
15 # for example, there is a path /tmp/tmp pointed to a file
16 # when we ls /tmp, it report one item named 'tmp' with type file,
17 # but actually it is a dir.
18 # so here we use another way to determine, that is, download it,
19 # you can NOT download a dir.
20 echo "dir or file exist, try download to check"
21 fi
22
23 if [ $ret -eq 3 ]; then
24 rm "$download" > /dev/null 2>&1 # otherwise bst_tool will fail
25 line=$(bst_tool get "$remote" "$download" | sed -n '2p')
26 str=${line/"download success"//}
27 if [ "$str" != "$line" ]; then # string replace changed: download success
28 # is a file
29 ret=1
30 else
31 # is a dir
32 ret=3
33 fi
34 fi

這裡為了避免將 size == 0 的場景也包含進來,在 size > 0 (> 1 及 == 1) 時將 ret 的值臨時設定為 3,之後再通過下載來判斷。下面是加入補丁後腳本的執行情況:

$ bstput data /tmp
dir exist, try download to check existence of file with same name
dir exist, try file
dir exist, try download to check existence of file with same name
remote file exist, do you want to cover it? (n/y) y
當前使用者:yunhai
deleting the 1th filePath: /tmp/data
delete result: 成功
當前使用者:yunhai
uploading... 100.00 %
upload success:
FileId: 425166451
FilePath: /tmp/data
Time: 2021-05-27 14:56:45

可以看到這回能正確的上傳了。不過這裡也有一些額外代價,看第一行輸出,本來檢查完 /tmp 是目錄就該結束了,但是依照新的邏輯,需要確認沒有一個叫 /tmp 的同名檔案,於是又去試下載 /tmp,增加了額外的非必要請求。不過權衡正反兩個方面,為了正確性做的這點效能犧牲還是值得的。

除了指令碼的 bug,其實細心的人已經發現,這個工具及其配套的後臺也有問題,就拿最開始企業網盤在瀏覽器裡那張截圖來說吧,裡面怎麼出現了兩個同名目錄 (/tmp) ? 其實就是我不斷的測試,不知怎麼著觸發了後臺的 bug,導致出現了兩個一模一樣的目錄項,在其中一個裡面修改內容,另一個也會跟著變化 (很明確不是檔名重複這樣簡單的問題)。聯絡過相關負責人,給的結論是這個東西已經停止維護,甚至準備下線了,所以也不再接收新的 bug report,當時差點暈倒,得,將就用吧~

結語

做這個命令擴充套件指令碼花了不少心血,不過可能由於工具本身不是開源的緣故,能拿過來直接用的可能性比較低,甚至想跑一跑都沒有環境 (與公司帳號系統繫結)。不過原理都是相通的,指令碼本身可以做為一種參考,這裡給出指令碼的 github 地址供觀摩:

[email protected]:goodpaperman/bstext.git

如果有幸能跑起來 (說明咱們是一個公司的?),可以定義以下環境變數來改變指令碼的行為:

  • BST_TOOL_VERBOSE:開啟除錯輸出,可以看到更多中間細節;
  • BST_TOOL_GET_HUGE:下載閾值,單位為 MB,大於此值的檔案均略過,不設定的話預設為 1 MB 。

參考

[1]. Shell判斷檔案或目錄是否存在

[2]. shell 字串包含

[3]. 那些年我用awk時踩過的坑——awk使用注意事項

[4]. shell指令碼中如何使用alias

[5]. Linux_shell自動輸入y或yes

[6]. awk使用shell變數及shell使用awk中的變數

[7]. Shell高階語法:awk配合eval實現快速變數