2022年9月19日 星期一

用 Java 進行 SSH 連線 - 使用 JSch

 在這篇文中要展示如何用 Java 以 JSch 這個 library 來進行 SSH 連線,

首先是用 Maven 引入 library :

<!-- https://mvnrepository.com/artifact/com.jcraft/jsch -->
<dependency>
	<groupId>com.jcraft</groupId>
	<artifactId>jsch</artifactId>
	<version>0.1.55</version>
</dependency>
接著先直接上程式碼:
package test;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;

public class SftpTest {

	public static void main(String[] args) throws Exception {
		
		String privateKeyPath = "D:\\...\xx.pem"; //your private key file path
		String userName = "xxName"; //your username to access target server
		String host = "xxx.xxx.xxx.xxx"; //hostname of target server
		int port = 22; //target server port
		
		Session session = null;
		
		try (InputStream inputStreamOfPrivateKey = new FileInputStream(new File(privateKeyPath))){			
	        JSch jsch = new JSch();	        
	         
	        jsch.addIdentity(privateKeyPath, inputStreamOfPrivateKey.readAllBytes(), null, null);
	        session = jsch.getSession(userName, host, port);
            //session.setPassword("xxxPassword");
	        session.setConfig("StrictHostKeyChecking", "no");
	        session.setTimeout(60000);
	        session.connect();
			
	        ChannelSftp channelSftp = (ChannelSftp) session.openChannel("sftp");
	        channelSftp.connect();
	        
	        //check if file exists
	        System.out.println(isFileExist(channelSftp, "/xxx/xxx.png"));
	        
	        //check file attributes
	        SftpATTRS fileAttrs = channelSftp.lstat("/xxx/xxx.png");
	        System.out.println(fileAttrs.getSize());
	        
	        //create directory
	        mkdir(channelSftp, "/xxx/xxx");
	        
	        //upload file
	        upload(channelSftp, "D:\\xxx\\xxx.jpg", "/xxx/xxx");

	        channelSftp.exit();
		}catch(Exception e) {
			e.printStackTrace();
		} finally {
			if (session != null && session.isConnected()) {
				session.disconnect();
			}			
		}
		System.out.println("Done");
	}
	
	public static void upload(ChannelSftp channelSftp, String srcFilePath, String targetDirectoryPath) {
    	targetDirectoryPath = targetDirectoryPath.replace("\\", "/");
    	
    	File srcFile = new File(srcFilePath);
    	try (FileInputStream fileInputStream = new FileInputStream(srcFile)){
    		if (isFileExist(channelSftp, targetDirectoryPath)) {
        		channelSftp.cd(targetDirectoryPath);
        	}else {
        		channelSftp.cd("/"); //go to root path
        		
        		String[] targetDirectoryNameList = targetDirectoryPath.split("/");
        		for(String targetDirectoryName : targetDirectoryNameList) {
        			if ("".equals(targetDirectoryName)) {
        				continue;
        			}
        			if (!isFileExist(channelSftp, targetDirectoryName)) {
        				channelSftp.mkdir(targetDirectoryName);
        			}
        			channelSftp.cd(targetDirectoryName);
        		}
        	}
    		channelSftp.put(fileInputStream, srcFile.getName());
    	} catch (IOException | SftpException e) {
			e.printStackTrace();
		}
    }
	
	static void mkdir(ChannelSftp channelSftp, String directoryPath) {
		directoryPath = directoryPath.replace("\\", "/");

    	try {
    		channelSftp.cd("/"); //go to root path
    		
    		String[] directoryNameList = directoryPath.split("/");
    		for(String directoryName : directoryNameList) {
    			if ("".equals(directoryName)) {
    				continue;
    			}
    			if (!isFileExist(channelSftp, directoryName)) {
    				channelSftp.mkdir(directoryName);
    			}
    			channelSftp.cd(directoryName);
    		}
    	} catch (SftpException e) {
			e.printStackTrace();
		}
	}
	
	static boolean isFileExist(ChannelSftp channelSftp, String filePath) {
		boolean isFileExist = false;
		
		try {
			channelSftp.lstat(filePath);
			isFileExist = true;
		} catch (SftpException e) {
			if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
				isFileExist = false;
			}
		}
		
		return isFileExist;
	}

}

說明:
上面程式碼的情境是想要 SSH 連線至一台 Linux Server,
且已經設定好 ssh public key 並放在 server 的 .ssh 資料夾裡,
而我們本地的機器上也已經有一個對應的 ssh private key,
所以我們只需使用 private key 來進行連線而不用使用密碼。

session.setConfig("StrictHostKeyChecking", "no");
這行程式碼設定了 StrictHostKeyChecking 為 no,
是因為在 SSH 連線時有時會需要終端機互動,
例如在使用命令式模式指令進行 SSH 連線操作時,
server 可能會跳出一些訊息要你輸入 "yes" 等動作才能進行下一步,
設定 StrictHostKeyChecking 為 no 就可以避免這種互動式操作,
可參考:

在程式碼中可以看到,取得了 ChannelSftp 實例後,
就可以使用它來進行對 server 的操作,例如: cd, pwd, ls, mkdir, ... 等,
就像我們平台用命令列模式指令一樣,當使用了 cd 指令後,
會把我們帶到特定的 server 上的檔案路徑位置,
並且可以以目前所在的位置用相對路徑進行 ls, mkdir, ... 等操作,
當然也可使用絕對路徑進行操作。

在上述程式碼中,
我包裝並實作了 isFileExist(), upload(), mkdir() 等 methild 
來取代直接使用 ChannelSftp 自身的 API,
在實作的 method 中限定了檔案路徑參數必須為絕對路徑以避免不必要的
路徑處理麻煩,
isFileExist() 使用了 ChannelSftp.lstat() 和 ChannelSftp.SSH_FX_NO_SUCH_FILE 的補獲來實作。
upload() 和 mkdir() 使用 isFileExist() 來判斷路徑上資料夾的存在與否,
如果不存在的話則幫忙建立相應的資料夾。

2022年9月18日 星期日

在同一個頁面上建立多個 AngularJs 的 module 及 controller - 使用 angular.bootstrap()

ng-app 是很常見的 AngularJs 頁面 module 的設定,
但這樣的方式會有局限性,
因為 AngularJs 只允許一個頁面使用一個 ng-app,
如果有多個 ng-app 則會錯誤或不正常運作。

不過這並不代表 AngularJs 不能在一個頁面上設定多個 module,
AngularJs 可以使用
angular.bootstrap(element, moduleNameArray)
來在一個頁面上設定多個 module,
其中參數 element 是 module 所在的 DOM Object,
而 moduleNameArray 則是個 array<String>,
內部放著要設定到頁面上的 module 名稱。

以下是一個範例,
頁面上有三個要被設定 module 的 DOM Object,
分別是 id="module_1" 、 id="module_2" 和 id="module_3",
然後我們會先設定兩個  AngularJs module,module 名為 "module_A" 和 "module_B",
各 module 裡面各有一個名為 "controller" 的 controller,
我們要把 module_A 設定到 #module_1 和 #module_2 上,
及把 module_A 和 module_B 一起設定到 #module_3 上。

html:
<div id="module_1">
  module_1:
  <div ng-controller="controller as ctrl">
    <input type="text" ng-model="ctrl.text"/>
  </div>
</div>

<div id="module_2">
  module_2:
  <div ng-controller="controller as ctrl">
    <input type="text" ng-model="ctrl.text"/>
  </div>
  
   <div ng-controller="controller as ctrl">
    <input type="text" ng-model="ctrl.text"/>
  </div>
</div>

<div id="module_3">
  module_3:
  <div ng-controller="controller as ctrl">
    <input type="text" ng-model="ctrl.text"/>
  </div>
  
  <div ng-controller="controller as ctrl">
    <input type="text" ng-model="ctrl.text"/>
  </div>
</div>
Javascript:
angular.module("module_A", [])
.controller("controller", [function(){
	var self = this;
  self.text = "A";
}])

angular.module("module_B", [])
.controller("controller", [function(){
	var self = this;
  self.text = "B";
}]);

angular.bootstrap(document.getElementById("module_1"), ["module_A"]);
angular.bootstrap(document.getElementById("module_2"), ["module_A"]);
angular.bootstrap(document.getElementById("module_3"), ["module_A", "module_B"]);
以下是 Jsfiddle 上的成品:
說明:
首先可以先注意到,在 html 中的
id="module_2" 中
故意擺放了名稱一樣的 controller:
ng-controller="controller as ctrl"
這裡是要展示一個特性,就是即使同一個 module 下有相同 controller 名稱的 controller,
也就是它們都使用了在 javascript 中同一個 module 之下的同樣名為 "controller" 的 controller 設定,
它們的 scope 仍然是不同的,方便想像可以把它們看做是同一個 function 所 new 出來的不同  Class 實體 (Class Instance),
所以它們設定的 ng-model 是各自獨立的,並不會互相連動,
可以從 Jsfiddle 上的成品範例實際使用觀察看看。

再來在 html 中的
id="module_1" 和 id="module_2" 雖然都用了在 javascript 中相同的名為 "module_A" 的 module 設定,
不過它們仍然算是互相獨立的不同 module 個體,跟之前同個 module 下的同名 controller 說明很像,
所以它們彼此的同名 controller 的 ng-model 也是不會互相連動的。

最後是在 html 中的
id="module_3",它被設定了 module_A 和 module_B,
所以它可以得到 module_A 之下及 module_B 之下的 controller 設定,
有點像是一個載入了 module_A 和 module_B 的 module_C 一樣,
可以方便想像成如下:
angular.module("module_C", ["module_A", "module_B"]),
然後當然的,跟上面說明的情況一樣,
id="module_3" 上設定的 module 跟 controller 一樣是獨立於 id="module_1" 和 id="module_2",
事實上,id="module_1"、id="module_2" 和 id="module_3" 上的 module 跟 controller 彼此都是互相獨立無關的,
唯一的共同點就只有都有使用到了 module_A 的設定而已。

最後的結果是範例中四個 <input> 的值彼此都不是互相連動的。
這在如果你想要把相同程式邏輯的 module 做成元件,
並放到頁面上的各處,又不想它們彼此共用 scope (即共用 ng-model 去連動改變 scope 中的變數值) 時,
會是一個可利用的不錯的特性。

是 Luis 作者寫的一篇關於 ng-app 限制的文章,
也一樣用到了 angular.bootstrap 去解決 ng-app 限制的問題,
並在 Github 上實作了一個方便來用 DOM 屬性設定 AngularJs module 的 module 工具,
源碼部份也是使用了一樣的觀念,
找出要設定 module 的 DOM,從屬性中讀到要被設定的 module 名稱後進行 module 的設定,
十分地具有參考的價值。