【轉】JSch - Java實現的SFTP(文件上傳詳解篇)

  JSch是Java Secure Channel的縮寫。JSch是一個SSH2的純Java實現。它容許你鏈接到一個SSH服務器,而且可使用端口轉發,X11轉發,文件傳輸等,固然你也能夠集成它的功能到你本身的應用程序。html

  本文只介紹如何使用JSch實現的SFTP功能。java

  SFTP是Secure File Transfer Protocol的縮寫,安全文件傳送協議。能夠爲傳輸文件提供一種安全的加密方法。SFTP 爲 SSH的一部份,是一種傳輸文件到服務器的安全方式。SFTP是使用加密傳輸認證信息和傳輸的數據,因此,使用SFTP是很是安全的。可是,因爲這種傳輸方式使用了加密/解密技術,因此傳輸效率比普通的FTP要低得多,若是您對網絡安全性要求更高時,可使用SFTP代替FTP。(來自百度的解釋) apache

  要使用JSch,須要下載它的jar包,請從官網下載它:http://www.jcraft.com/jsch/安全

  ChannelSftp類是JSch實現SFTP核心類,它包含了全部SFTP的方法,如:服務器

put():      文件上傳網絡

get():      文件下載session

cd():       進入指定目錄ide

ls():       獲得指定目錄下的文件列表函數

rename():   重命名指定文件或目錄工具

rm():       刪除指定文件

mkdir():    建立目錄

rmdir():    刪除目錄

等等(這裏省略了方法的參數,put和get都有多個重載方法,具體請看源代碼,這裏不一一列出。)

  JSch支持三種文件傳輸模式:

OVERWRITE 徹底覆蓋模式,這是JSch的默認文件傳輸模式,即若是目標文件已經存在,傳輸的文件將徹底覆蓋目標文件,產生新的文件。
RESUME

恢復模式,若是文件已經傳輸一部分,這時因爲網絡或其餘任何緣由致使文件傳輸中斷,若是下一次傳輸相同的文件,

則會從上一次中斷的地方續傳。

APPEND 追加模式,若是目標文件已存在,傳輸的文件將在目標文件後追加。

建立ChannelSftp對象

  編寫一個工具類,根據ip,用戶名及密碼獲得一個SFTP channel對象,即ChannelSftp的實例對象,在應用程序中就可使用該對象來調用SFTP的各類操做方法。

 1 package com.longyg.sftp;
 2 
 3 import java.util.Map;
 4 import java.util.Properties;
 5 import org.apache.log4j.Logger;
 6 import com.jcraft.jsch.Channel;
 7 import com.jcraft.jsch.ChannelSftp;
 8 import com.jcraft.jsch.JSch;
 9 import com.jcraft.jsch.JSchException;
10 import com.jcraft.jsch.Session;
11 public class SFTPChannel {
12     Session session = null;
13     Channel channel = null;
14     private static final Logger LOG = Logger.getLogger(SFTPChannel.class.getName());
15     public ChannelSftp getChannel(Map<String, String> sftpDetails, int timeout) throws JSchException {
16         String ftpHost = sftpDetails.get(SFTPConstants.SFTP_REQ_HOST);
17         String port = sftpDetails.get(SFTPConstants.SFTP_REQ_PORT);
18         String ftpUserName = sftpDetails.get(SFTPConstants.SFTP_REQ_USERNAME);
19         String ftpPassword = sftpDetails.get(SFTPConstants.SFTP_REQ_PASSWORD);
20         int ftpPort = SFTPConstants.SFTP_DEFAULT_PORT;
21         if (port != null && !port.equals("")) {
22             ftpPort = Integer.valueOf(port);
23         }
24         JSch jsch = new JSch(); // 建立JSch對象
25         session = jsch.getSession(ftpUserName, ftpHost, ftpPort); // 根據用戶名,主機ip,端口獲取一個Session對象
26         LOG.debug("Session created.");
27         if (ftpPassword != null) {
28             session.setPassword(ftpPassword); // 設置密碼
29         }
30         Properties config = new Properties();
31         config.put("StrictHostKeyChecking", "no");
32         session.setConfig(config); // 爲Session對象設置properties
33         session.setTimeout(timeout); // 設置timeout時間
34         session.connect(); // 經過Session創建連接
35         LOG.debug("Session connected.");
36         LOG.debug("Opening Channel.");
37         channel = session.openChannel("sftp"); // 打開SFTP通道
38         channel.connect(); // 創建SFTP通道的鏈接
39         LOG.debug("Connected successfully to ftpHost = " + ftpHost + ",as ftpUserName = " + ftpUserName
40                 + ", returning: " + channel);
41         return (ChannelSftp) channel;
42     }
43     public void closeChannel() throws Exception {
44         if (channel != null) {
45             channel.disconnect();
46         }
47         if (session != null) {
48             session.disconnect();
49         }
50     }
51 }
SFTPChannel.java

 

SFTPConstants是一個靜態成員變量類:

 1 package com.longyg.sftp;
 2 
 3 public class SFTPConstants {
 4     public static final String SFTP_REQ_HOST = "host";
 5     public static final String SFTP_REQ_PORT = "port";
 6     public static final String SFTP_REQ_USERNAME = "username";
 7     public static final String SFTP_REQ_PASSWORD = "password";
 8     public static final int SFTP_DEFAULT_PORT = 22;
 9     public static final String SFTP_REQ_LOC = "location";
10 }
SFTPConstants.java

 

文件上傳

  實現文件上傳能夠調用ChannelSftp對象的put方法。ChannelSftp中有12個put方法的重載方法:

public void put(String src, String dst)

將本地文件名爲src的文件上傳到目標服務器,目標文件名爲dst,若dst爲目錄,則目標文件名將與src文件名相同。

採用默認的傳輸模式:OVERWRITE

public void put(String src, String dst, int mode)

將本地文件名爲src的文件上傳到目標服務器,目標文件名爲dst,若dst爲目錄,則目標文件名將與src文件名相同。

指定文件傳輸模式爲mode(mode可選值爲:ChannelSftp.OVERWRITE,ChannelSftp.RESUME,

ChannelSftp.APPEND)

 

public void put(String src, String dst, SftpProgressMonitor monitor)

將本地文件名爲src的文件上傳到目標服務器,目標文件名爲dst,若dst爲目錄,則目標文件名將與src文件名相同。

採用默認的傳輸模式:OVERWRITE

並使用實現了SftpProgressMonitor接口的monitor對象來監控文件傳輸的進度。

public void put(String src, String dst, 
SftpProgressMonitor monitor, int mode)

將本地文件名爲src的文件上傳到目標服務器,目標文件名爲dst,若dst爲目錄,則目標文件名將與src文件名相同。

指定傳輸模式爲mode

並使用實現了SftpProgressMonitor接口的monitor對象來監控文件傳輸的進度。

public void put(InputStream src, String dst)

將本地的input stream對象src上傳到目標服務器,目標文件名爲dst,dst不能爲目錄。

採用默認的傳輸模式:OVERWRITE

public void put(InputStream src, String dst, int mode)

將本地的input stream對象src上傳到目標服務器,目標文件名爲dst,dst不能爲目錄。

指定文件傳輸模式爲mode

public void put(InputStream src, String dst, SftpProgressMonitor monitor)

將本地的input stream對象src上傳到目標服務器,目標文件名爲dst,dst不能爲目錄。

採用默認的傳輸模式:OVERWRITE

並使用實現了SftpProgressMonitor接口的monitor對象來監控傳輸的進度。

public void put(InputStream src, String dst, 
SftpProgressMonitor monitor, int mode)

將本地的input stream對象src上傳到目標服務器,目標文件名爲dst,dst不能爲目錄。

指定文件傳輸模式爲mode

並使用實現了SftpProgressMonitor接口的monitor對象來監控傳輸的進度。

public OutputStream put(String dst)

該方法返回一個輸出流,能夠向該輸出流中寫入數據,最終將數據傳輸到目標服務器,目標文件名爲dst,dst不能爲目錄。

採用默認的傳輸模式:OVERWRITE

public OutputStream put(String dst, final int mode)

該方法返回一個輸出流,能夠向該輸出流中寫入數據,最終將數據傳輸到目標服務器,目標文件名爲dst,dst不能爲目錄。

指定文件傳輸模式爲mode

public OutputStream put(String dst, final SftpProgressMonitor monitor, final int mode) 

該方法返回一個輸出流,能夠向該輸出流中寫入數據,最終將數據傳輸到目標服務器,目標文件名爲dst,dst不能爲目錄。

指定文件傳輸模式爲mode

並使用實現了SftpProgressMonitor接口的monitor對象來監控傳輸的進度。

public OutputStream put(String dst, final SftpProgressMonitor monitor, final int mode, long offset)

該方法返回一個輸出流,能夠向該輸出流中寫入數據,最終將數據傳輸到目標服務器,目標文件名爲dst,dst不能爲目錄。

指定文件傳輸模式爲mode

並使用實現了SftpProgressMonitor接口的monitor對象來監控傳輸的進度。

offset指定了一個偏移量,從輸出流偏移offset開始寫入數據。

 

應用實例:

 1 package com.longyg.sftp;
 2 
 3 import java.util.HashMap;
 4 import java.util.Map;
 5 import com.jcraft.jsch.ChannelSftp;
 6 public class SFTPTest {
 7     public SFTPChannel getSFTPChannel() {
 8         return new SFTPChannel();
 9     }
10     /**
11      * @param args
12      * @throws Exception
13      */
14     public static void main(String[] args) throws Exception {
15         SFTPTest test = new SFTPTest();
16         Map<String, String> sftpDetails = new HashMap<String, String>();
17         // 設置主機ip,端口,用戶名,密碼
18         sftpDetails.put(SFTPConstants.SFTP_REQ_HOST, "10.9.167.55");
19         sftpDetails.put(SFTPConstants.SFTP_REQ_USERNAME, "root");
20         sftpDetails.put(SFTPConstants.SFTP_REQ_PASSWORD, "arthur");
21         sftpDetails.put(SFTPConstants.SFTP_REQ_PORT, "22");
22         
23         String src = "D:\\DevSoft\\HB-SnagIt1001.rar"; // 本地文件名
24         String dst = "/home/omc/ylong/sftp/HB-SnagIt1001.rar"; // 目標文件名
25               
26         SFTPChannel channel = test.getSFTPChannel();
27         ChannelSftp chSftp = channel.getChannel(sftpDetails, 60000);
28         
29         /**
30          * 代碼段1
31         OutputStream out = chSftp.put(dst, ChannelSftp.OVERWRITE); // 使用OVERWRITE模式
32         byte[] buff = new byte[1024 * 256]; // 設定每次傳輸的數據塊大小爲256KB
33         int read;
34         if (out != null) {
35             System.out.println("Start to read input stream");
36             InputStream is = new FileInputStream(src);
37             do {
38                 read = is.read(buff, 0, buff.length);
39                 if (read > 0) {
40                     out.write(buff, 0, read);
41                 }
42                 out.flush();
43             } while (read >= 0);
44             System.out.println("input stream read done.");
45         }
46         **/
47         
48         chSftp.put(src, dst, ChannelSftp.OVERWRITE); // 代碼段2
49         
50         // chSftp.put(new FileInputStream(src), dst, ChannelSftp.OVERWRITE); // 代碼段3
51         
52         chSftp.quit();
53         channel.closeChannel();
54     }
55 }
SFTPTest.java

 

:請分別將代碼段1,代碼段2,代碼段3取消註釋,運行程序來進行測試。這三段代碼分別演示瞭如何使用JSch的不一樣的put方法來進行文件上傳。

代碼段1:採用向put方法返回的輸出流中寫入數據的方式來傳輸文件。 須要由程序來決定寫入什麼樣的數據,這裏是將本地文件的輸入流寫入輸出流。採用這種方式的好處是,能夠自行設定每次寫入輸出流的數據塊大小,如本示例中的語句:

byte[] buff = new byte[1024 * 256]; // 設定每次傳輸的數據塊大小爲256KB

代碼段2:直接將本地文件名爲src的文件上傳到目標服務器,目標文件名爲dst。(注:使用這個方法時,dst能夠是目錄,當dst是目錄時,上傳後的目標文件名將與src文件名相同)

代碼段3:將本地文件名爲src的文件輸入流上傳到目標服務器,目標文件名爲dst。

這三段代碼實現的功能是同樣的,都是將本地的文件src上傳到了服務器的dst文件。使用時可根據具體狀況選擇使用哪一種實現方式。

 

監控傳輸進度

從前面的介紹中知道,JSch支持在文件傳輸時對傳輸進度的監控。能夠實現JSch提供的SftpProgressMonitor接口來完成這個功能。

SftpProgressMonitor接口類的定義爲:

1 package com.jcraft.jsch;
2 
3 public interface SftpProgressMonitor{
4   public static final int PUT=0;
5   public static final int GET=1;
6   void init(int op, String src, String dest, long max);
7   boolean count(long count);
8   void end();
9 }
SftpProgressMonitor.java

 

init():    當文件開始傳輸時,調用init方法。

count():   當每次傳輸了一個數據塊後,調用count方法,count方法的參數爲這一次傳輸的數據塊大小。

end():     當傳輸結束時,調用end方法。

 

下面是一個簡單的實現:

 1 package com.longyg.sftp;
 2 
 3 import com.jcraft.jsch.SftpProgressMonitor;
 4 
 5 public class MyProgressMonitor implements SftpProgressMonitor {
 6     private long transfered;
 7     @Override
 8     public boolean count(long count) {
 9         transfered = transfered + count;
10         System.out.println("Currently transferred total size: " + transfered + " bytes");
11         return true;
12     }
13     @Override
14     public void end() {
15         System.out.println("Transferring done.");
16     }
17     @Override
18     public void init(int op, String src, String dest, long max) {
19         System.out.println("Transferring begin.");
20     }
21 }
MyProgressMonitor.java

 

此時若是改變SFTPTest main方法裏調用的put方法,便可實現監控傳輸進度:

 1 package com.longyg.sftp;
 2 
 3 import java.util.HashMap;
 4 import java.util.Map;
 5 import com.jcraft.jsch.ChannelSftp;
 6 
 7 public class SFTPTest {
 8     public SFTPChannel getSFTPChannel() {
 9         return new SFTPChannel();
10     }
11     /**
12      * @param args
13      * @throws Exception
14      */
15     public static void main(String[] args) throws Exception {
16         SFTPTest test = new SFTPTest();
17         Map<String, String> sftpDetails = new HashMap<String, String>();
18         // 設置主機ip,端口,用戶名,密碼
19         sftpDetails.put(SFTPConstants.SFTP_REQ_HOST, "10.9.167.55");
20         sftpDetails.put(SFTPConstants.SFTP_REQ_USERNAME, "root");
21         sftpDetails.put(SFTPConstants.SFTP_REQ_PASSWORD, "arthur");
22         sftpDetails.put(SFTPConstants.SFTP_REQ_PORT, "22");
23         
24         String src = "D:\\DevSoft\\HB-SnagIt1001.rar"; // 本地文件名
25         String dst = "/home/omc/ylong/sftp/HB-SnagIt1001.rar"; // 目標文件名
26               
27         SFTPChannel channel = test.getSFTPChannel();
28         ChannelSftp chSftp = channel.getChannel(sftpDetails, 60000);
29         
30         /**
31          * 代碼段1
32         OutputStream out = chSftp.put(dst, new MyProgressMonitor(), ChannelSftp.OVERWRITE); // 使用OVERWRITE模式
33         byte[] buff = new byte[1024 * 256]; // 設定每次傳輸的數據塊大小爲256KB
34         int read;
35         if (out != null) {
36             System.out.println("Start to read input stream");
37             InputStream is = new FileInputStream(src);
38             do {
39                 read = is.read(buff, 0, buff.length);
40                 if (read > 0) {
41                     out.write(buff, 0, read);
42                 }
43                 out.flush();
44             } while (read >= 0);
45             System.out.println("input stream read done.");
46         }
47         **/
48         
49         chSftp.put(src, dst, new MyProgressMonitor(), ChannelSftp.OVERWRITE); // 代碼段2
50         
51         // chSftp.put(new FileInputStream(src), dst, new MyProgressMonitor(), ChannelSftp.OVERWRITE); // 代碼段3
52         
53         chSftp.quit();
54         channel.closeChannel();
55     }
56 }
SFTPTest.java

 

注意修改的內容僅僅是put方法,在put方法中增長了SftpProgressMonitor的實現類對象monitor做爲參數,即添加了對進度監控的支持。

運行,輸出結果以下:

 1 Start to read input stream
 2 Currently transferred total size: 262144 bytes
 3 Currently transferred total size: 524288 bytes
 4 Currently transferred total size: 786432 bytes
 5 Currently transferred total size: 1048576 bytes
 6 Currently transferred total size: 1310720 bytes
 7 Currently transferred total size: 1572864 bytes
 8 Currently transferred total size: 1835008 bytes
 9 Currently transferred total size: 2097152 bytes
10 Currently transferred total size: 2359296 bytes
11 Currently transferred total size: 2621440 bytes
12 Currently transferred total size: 2883584 bytes
13 Currently transferred total size: 3145728 bytes
14 Currently transferred total size: 3407872 bytes
15 Currently transferred total size: 3670016 bytes
16 Currently transferred total size: 3848374 bytes
17 input stream read done.

 

固然這個SftpProgressMonitor的實現實在太簡單。JSch每次傳輸一個數據塊,就會調用count方法來實現主動進度通知。

如今咱們但願每間隔必定的時間才獲取一下文件傳輸的進度。。。看看下面的SftpProgressMonitor實現:

  1 package com.longyg.sftp;
  2 
  3 import java.text.DecimalFormat;
  4 import java.util.Timer;
  5 import java.util.TimerTask;
  6 import com.jcraft.jsch.SftpProgressMonitor;
  7 
  8 public class FileProgressMonitor extends TimerTask implements SftpProgressMonitor {
  9     
 10     private long progressInterval = 5 * 1000; // 默認間隔時間爲5秒
 11     
 12     private boolean isEnd = false; // 記錄傳輸是否結束
 13     
 14     private long transfered; // 記錄已傳輸的數據總大小
 15     
 16     private long fileSize; // 記錄文件總大小
 17     
 18     private Timer timer; // 定時器對象
 19     
 20     private boolean isScheduled = false; // 記錄是否已啓動timer記時器
 21     
 22     public FileProgressMonitor(long fileSize) {
 23         this.fileSize = fileSize;
 24     }
 25     
 26     @Override
 27     public void run() {
 28         if (!isEnd()) { // 判斷傳輸是否已結束
 29             System.out.println("Transfering is in progress.");
 30             long transfered = getTransfered();
 31             if (transfered != fileSize) { // 判斷當前已傳輸數據大小是否等於文件總大小
 32                 System.out.println("Current transfered: " + transfered + " bytes");
 33                 sendProgressMessage(transfered);
 34             } else {
 35                 System.out.println("File transfering is done.");
 36                 setEnd(true); // 若是當前已傳輸數據大小等於文件總大小,說明已完成,設置end
 37             }
 38         } else {
 39             System.out.println("Transfering done. Cancel timer.");
 40             stop(); // 若是傳輸結束,中止timer記時器
 41             return;
 42         }
 43     }
 44     
 45     public void stop() {
 46         System.out.println("Try to stop progress monitor.");
 47         if (timer != null) {
 48             timer.cancel();
 49             timer.purge();
 50             timer = null;
 51             isScheduled = false;
 52         }
 53         System.out.println("Progress monitor stoped.");
 54     }
 55     
 56     public void start() {
 57         System.out.println("Try to start progress monitor.");
 58         if (timer == null) {
 59             timer = new Timer();
 60         }
 61         timer.schedule(this, 1000, progressInterval);
 62         isScheduled = true;
 63         System.out.println("Progress monitor started.");
 64     }
 65     
 66     /**
 67      * 打印progress信息
 68      * @param transfered
 69      */
 70     private void sendProgressMessage(long transfered) {
 71         if (fileSize != 0) {
 72             double d = ((double)transfered * 100)/(double)fileSize;
 73             DecimalFormat df = new DecimalFormat( "#.##");
 74             System.out.println("Sending progress message: " + df.format(d) + "%");
 75         } else {
 76             System.out.println("Sending progress message: " + transfered);
 77         }
 78     }
 79     /**
 80      * 實現了SftpProgressMonitor接口的count方法
 81      */
 82     public boolean count(long count) {
 83         if (isEnd()) return false;
 84         if (!isScheduled) {
 85             start();
 86         }
 87         add(count);
 88         return true;
 89     }
 90     /**
 91      * 實現了SftpProgressMonitor接口的end方法
 92      */
 93     public void end() {
 94         setEnd(true);
 95         System.out.println("transfering end.");
 96     }
 97     
 98     private synchronized void add(long count) {
 99         transfered = transfered + count;
100     }
101     
102     private synchronized long getTransfered() {
103         return transfered;
104     }
105     
106     public synchronized void setTransfered(long transfered) {
107         this.transfered = transfered;
108     }
109     
110     private synchronized void setEnd(boolean isEnd) {
111         this.isEnd = isEnd;
112     }
113     
114     private synchronized boolean isEnd() {
115         return isEnd;
116     }
117     public void init(int op, String src, String dest, long max) {
118         // Not used for putting InputStream
119     }
120 }
FileProgressMonitor.java

 

再次修改SFTPTest main方法裏的put方法,改成使用新的SftpProgressMonitor的實現類對象monitor做爲參數,注意新的monitor對象的構造函數須要傳入文件大小做爲參數:

 1 package com.longyg.sftp;
 2 
 3 import java.io.File;
 4 import java.util.HashMap;
 5 import java.util.Map;
 6 import com.jcraft.jsch.ChannelSftp;
 7 
 8 public class SFTPTest {
 9     public SFTPChannel getSFTPChannel() {
10         return new SFTPChannel();
11     }
12     /**
13      * @param args
14      * @throws Exception
15      */
16     public static void main(String[] args) throws Exception {
17         SFTPTest test = new SFTPTest();
18         Map<String, String> sftpDetails = new HashMap<String, String>();
19         // 設置主機ip,端口,用戶名,密碼
20         sftpDetails.put(SFTPConstants.SFTP_REQ_HOST, "10.9.167.55");
21         sftpDetails.put(SFTPConstants.SFTP_REQ_USERNAME, "root");
22         sftpDetails.put(SFTPConstants.SFTP_REQ_PASSWORD, "arthur");
23         sftpDetails.put(SFTPConstants.SFTP_REQ_PORT, "22");
24         
25         String src = "D:\\DevSoft\\HB-SnagIt1001.rar"; // 本地文件名
26         String dst = "/home/omc/ylong/sftp/HB-SnagIt1001.rar"; // 目標文件名
27               
28         SFTPChannel channel = test.getSFTPChannel();
29         ChannelSftp chSftp = channel.getChannel(sftpDetails, 60000);
30         
31         File file = new File(src);
32         long fileSize = file.length();
33         
34         /**
35          * 代碼段1
36         OutputStream out = chSftp.put(dst, new FileProgressMonitor(fileSize), ChannelSftp.OVERWRITE); // 使用OVERWRITE模式
37         byte[] buff = new byte[1024 * 256]; // 設定每次傳輸的數據塊大小爲256KB
38         int read;
39         if (out != null) {
40             System.out.println("Start to read input stream");
41             InputStream is = new FileInputStream(src);
42             do {
43                 read = is.read(buff, 0, buff.length);
44                 if (read > 0) {
45                     out.write(buff, 0, read);
46                 }
47                 out.flush();
48             } while (read >= 0);
49             System.out.println("input stream read done.");
50         }
51         **/
52         
53         chSftp.put(src, dst, new FileProgressMonitor(fileSize), ChannelSftp.OVERWRITE); // 代碼段2
54         
55         // chSftp.put(new FileInputStream(src), dst, new FileProgressMonitor(fileSize), ChannelSftp.OVERWRITE); // 代碼段3
56         
57         chSftp.quit();
58         channel.closeChannel();
59     }
60 }
SFTPTest.java

 

再次運行,結果輸出爲: 

 1 Try to start progress monitor.
 2 Progress monitor started.
 3 Transfering is in progress.
 4 Current transfered: 98019 bytes
 5 Sending progress message: 2.55%
 6 Transfering is in progress.
 7 Current transfered: 751479 bytes
 8 Sending progress message: 19.53%
 9 Transfering is in progress.
10 Current transfered: 1078209 bytes
11 Sending progress message: 28.02%
12 ......
13 Transfering is in progress.
14 Current transfered: 3430665 bytes
15 Sending progress message: 89.15%
16 transfering end.
17 Transfering done. Cancel timer.
18 Try to stop progress monitor.
19 Progress monitor stoped.

 

如今,程序每隔5秒鐘纔會打印一下進度信息。能夠修改FileProgressMonitor類裏的progressInterval變量的值,來修改默認的間隔時間。

 


 

  原文連接:http://www.cnblogs.com/longyg/archive/2012/06/25/2556576.html

相關文章
相關標籤/搜索