1. 程式人生 > >基於ftp實現一個類dropbox檔案同步程式

基於ftp實現一個類dropbox檔案同步程式

最近要在實驗室和宿舍兩頭跑,同一臺電腦上還有win和linux等多個系統,想要在任何一個地點和平臺上繼續看之前的文獻實在有點麻煩,只能拿U盤來拷,但是我的U盤又很容易掉,萬一掉了那辛苦工作的成果可就全沒了。你說網盤吧,百度網盤又不合我意,同步太麻煩了。最符合我想法的還是dropbox, 只要把檔案丟在資料夾裡面,就會自動幫你同步。但是dropbox被封了呀, 有人推薦金山快盤,跑去一看已經關了。其他的檔案同步工具基本都是在win下的,不符合我雙系統的要求。得,正好手上有個騰訊的學生伺服器,那既然沒有現成的,就自己寫一個唄。

主要步驟如下:

  1. 獲得一個雲伺服器 (如果只是區域網內用,可選區域網中一臺電腦做主機)
  2. 配置ssh,免密碼登陸(這一步可以跳過,不影響後面的步驟)
  3. 搭建ftp伺服器,下載vsftpd並進行配置
  4. 對本地和ftp中的內容進行遍歷
  5. 比對兩邊遍歷結果,選擇相關的內容進行上傳,下載或刪除

由於我之前已經有了雲伺服器,所以獲取雲伺服器的部分就先跳過了,可以選擇騰訊雲或者阿里雲。兩者都有學生計劃,照著官網來就行了,很容易的。

1. ssh和sshfs相關配置(可跳過)

這一部分不是必須的。 ssh可以讓我們遠端登陸主機,方便在雲主機裡的操作。如果可以直接接觸到主機(例如在區域網中),那就沒有必要配置ssh了。
sshfs是建立的ssh的基礎上的。可以將遠端主機中的檔案對映到本地資料夾

,這樣就可以像操控本地檔案一樣操控遠端主機中的檔案了。

1.1 ssh登陸雲主機

一般來講,如果沒有特別設定過的話,ssh是可以用密碼登陸的, 像下面這樣。

ssh user@ip
ssh -p port user@ip

-p表示指定登陸的埠。ssh預設的埠是22。 輸入以後,會要求你輸入密碼,如果輸入正確的話,就可以進入雲主機了。 輸入exit即可退出。

但是騰訊的雲主機預設是不能用密碼進行登陸的,進入 /etc/ssh/sshd_config檢視,可以發現其PasswordAuthentication 一項是設為no的。即只可以用rsa私鑰進行登陸。

這裡寫圖片描述

碰到這種情況, 首先從網站上下載對應的私鑰, 假如名字為isa_key, 那麼在當前資料夾下,可以通過下面這種方法來登陸

ssh -i isa_key user@ip

還有一種方法,可以直接不需要密碼,也不需要指定私鑰檔案來登陸。首先使用

 ssh-keygen -t rsa 

來生成一個祕鑰對, 祕鑰和公鑰。私鑰一般放在/home/usr/.ssh資料夾內。其中usr為本地使用者自己的名字。然後將公鑰內容想辦法拷到雲主機裡面,假設名字為id_rsa.pub. 然後執行

 cat id_rsa.pub >> /home/remote_usr/.ssh/authorized_keys

這行程式碼的意思是,將id_rsa.pub中的內容附加到 authorized_keys 原有內容之後。>> 表示在原有內容之後附加,而 > 表示將原有內容覆蓋。在完成上述工作以後,即可執行ssh [email protected]來進行登陸了,不在需要輸入密碼或者指定私鑰檔案。

1.2 sshfs的使用

sshfs的功能之前也提到過了,可以將遠端的檔案對映到本地,可以像操作本地檔案一樣操作遠端檔案。
使用apt-get可以對sshfs進行安裝,裝完以後,記得先按照上一節的方法配置好ssh,因為sshfs是在ssh基礎上的。
sshfs的使用方法很簡單, 使用

 sshfs usr@ip:/ /home/tmp

即可將ip中以使用者usr將根目錄/下的檔案對映到本地的/home/tmp 目錄下。其中的路徑可以自由更改。掛載到本地目錄以後,效果如下所示

這裡寫圖片描述

這樣,就可以在本地操作遠端檔案了。非常的方便。
如果想取消掛載,可以使用

sudo umount /home/tmp 

來完成。

2. ftp的搭建和配置

首先在伺服器中使用sudo apt-get install vsftpd來安裝ftp伺服器。裝完以後應該自動運行了,可以檢視ftp://ip 來檢測。ip即為伺服器的ip。當然了,預設來說,ftp伺服器是隻能下載,不能上傳的,想要實現我之前說的檔案同步功能,上傳功能是必須要實現的,這就要修改vsftpd的配置檔案了。vsftpd的配置檔案在 /etc/vsftpd.conf 中。找到 write_enable =YES項,將前面的註釋符去掉,就行了。記得先執行

service vsftpd restart

將vsftpd重啟一下

3. python中ftplib的使用

在python3中,ftplib負責與ftp有關的操作。這部分就來講講ftplib

3.1 登陸與編碼

from ftp import FTP
ftp = FTP(ip)
ftp.login(user=user,passwd=passwd)
ftp.encoding = 'utf8'

如程式碼中所示,先建立ftp,然後進行登陸。值得一提的是下面的encoding.通過看原始碼可以發現,ftp連線預設的編碼是latin1, 而這個latin1是一個隱藏的炸彈,在上傳中文路徑的檔案時,就會提示無法解析。因此,需要提前將其編碼方式換成utf8

3.2 顯示當前資料夾下的檔案資訊及名字

顯示當前資料夾下有2中方法,一種是使用ftp.nlst()

from ftp import FTP
ftp = FTP(ip)
ftp.login(user=user,passwd=passwd)
ftp.encoding = 'utf8'   # 這部分之後不在出現,出現的ftp預設都是由此獲得的

ftp.cwd('/home/ftps')   # 指定ftp跳轉到/home/ftps 目錄下
print(ftp.nlst())       # 獲得/home/ftps目錄下的 
>>> ['syncdir']         # 因為此資料夾下只有syncdir目錄,所以只返回一個長度為1的陣列

第二種方法,使用ftp.retrlines(‘LIST’)

ftp.retrlines('LIST')
>>> drwxrwxrwx    4 1001     1001         4096 Aug 30 15:21 syncdir

這種方法返回的是比較詳細的資訊,drwxrwxrwx表示檔案的許可權,跟chmod得到的結果類似。d表示是資料夾,三個rwx分別表示檔案擁有者,所屬使用者組,以及其他使用者的讀,寫和執行許可權。
值得注意的是,通過原始碼發現,上述方法是可以加回調函式的。如果沒有自己指定回撥函式,就會使用預設的print_line函式。看名字也知道是輸出了,所以上面的內容儘管沒有用print函式,但它還是輸出在螢幕上了。那麼如果想要將這個結果進行進一步的處理怎麼辦呢?其實也好辦,像下面這樣

content = []
ftp.retrlines('LIST', callback=content.append)
print(content)
>>>['drwxrwxrwx    4 1001     1001         4096 Aug 30 15:21 syncdir']

其中的Aug 30 就是最近上傳的日期的名字

3.3 上傳,下載和刪除檔案

上傳檔案使用 ftp.storbinary()方法來實現

buffsize = 1024
local_file = open(local_file_path, 'rb')
ftp.storbinary('STOR ' + ftp_file_path, local_file, buffsize) 
ftp.close()
local_file.close()

其中local_file_path 和 ftp_file_path 分別為本地的檔案路徑和想要在ftp伺服器中儲存該檔案的路徑。這兩個在後面也有提到

下載檔案使用ftp.retrbinary()方法來實現

buffsize = 1024
local_file = open(local_file_path, 'wb')
ftp.retrbinary('RETR '+ftp_file_path, local_file.write, buffsize) 

還有其他的一些必要功能,羅列如下

ftp.rmd(path)       # 刪除資料夾
ftp.mkd(path)       # 新建資料夾
ftp.delete(path)    # 刪除檔案

4. 完成的程式

該程式規模比較小,所以就沒寫類,由幾個函式組成,重要的主要有

local_iterator(path)                    # 遍歷本地資料夾
remote_iterator(conn, path)     # 遍歷ftp端某個資料夾內所有檔案
pull()                                      # 從伺服器端同步到本地端
push()                                      # 從本地端同步到伺服器端

實際使用的時候,只要配置好config檔案,就可以直接使用pull和push進行同步了。而在config.py檔案內,需要手動填幾個變數:

local_dir       = 'C:\\Users\\multiangle\\Desktop\\paper'  # 要同步的本地資料夾

ftp_ip          = 你的 ftp 伺服器ip
ftp_user        = 使用者名稱
ftp_pwd         = 密碼
ftp_dir         = ftp伺服器中放置該檔案的位置。
from ftplib import FTP
import config
import re
import time
import os
import asyncio
from pprint import pprint
import logging


@asyncio.coroutine
def local_iterator(path=config.local_dir):
    file_paths = os.listdir(path)
    for file_path in file_paths:
        tmppath = os.path.join(path,file_path)
        if os.path.isdir(tmppath):
            try:
                yield local_stat(tmppath)
                yield from local_iterator(tmppath)
            except:
                pass
        else:
            try:
                yield local_stat(tmppath)
            except Exception as e:
                pass

def local_stat(tmppath):
    os_stat = os.stat(tmppath)
    st_mtime = os_stat.st_mtime
    rel_path_list = trans_abs_to_rel(config.local_dir,tmppath,os.sep)
    rel_path = "~"
    for x in rel_path_list:
        rel_path += '/' + x

    return dict(
        abs_path    = tmppath,
        rel_path    = rel_path,
        mtime       = int(st_mtime),
        size        = os_stat.st_size,
        isdir       = os.path.isdir(tmppath)
    )

@asyncio.coroutine
def remote_iterator(conn, path=config.ftp_dir):
    conn.cwd(path)
    lines = []
    abs_path = conn.pwd()
    conn.retrlines("LIST", lines.append)
    for line in lines:
        conn.cwd(path)
        info = parse_ftp_info(line)
        newpath = path + '/' + info['name']
        info['abs_path'] = newpath
        rel_path_list = trans_abs_to_rel(config.ftp_dir, newpath, '/')
        rel_path = '~'
        for x in rel_path_list:
            rel_path += '/' + x
        info['rel_path'] = rel_path
        if info['isdir']:
            yield info
            yield from remote_iterator(conn, path=newpath)
        else:
            # print(path)
            yield info

def parse_ftp_info(line):
    ret = {}
    current_year = 2016
    line = re.sub(r"\s+",' ',line)
    cutted_line = line.split(' ')
    if cutted_line.__len__()>9: # 如果檔案中有空格
        name = ''
        for i in range(8,cutted_line.__len__()):
            name += cutted_line[i]+' '
        name = name[:-1]
    else:
        name = cutted_line[-1]
    ret['auth']         = cutted_line[0]
    ret['name']         = name
    ret['time_str']     = '{y}-{m}-{d}-{t}'.format( y=current_year,
                                                    m=cutted_line[5],
                                                    d=cutted_line[6],
                                                    t=cutted_line[7])
    ret['mtime']    = int(time.mktime(time.strptime(ret['time_str'], '%Y-%b-%d-%H:%M')))
    ret['isdir']        = 'd' in ret['auth']
    return ret
# ftp.mkd('TEST')

def trans_abs_to_rel(par_p,abs_p,sep):
    par_l = par_p.split(sep)
    abs_l = abs_p.split(sep)
    rel_l = abs_l[par_l.__len__():]
    return rel_l

def pull():
    print('------------------------------------------------')
    print('start pull at {t}\n'.format(t=gen_time()))
    ftp = FTP(config.ftp_ip)
    ftp.encoding = 'utf-8'  # 這一步很重要,如果沒有會導致ftp無法解析中文檔名
    ftp.login(user=config.ftp_user,passwd=config.ftp_pwd)
    remote_files = [x for x in remote_iterator(ftp)]
    local_files = [x for x in local_iterator()]

    # 建立任務列表
    tasks = pull_task_gen(remote_files, local_files)

    # 根據任務列表進行更新,上傳,刪除等操作
    buffsize = 1024
    for task in tasks:
        if task['type'] == 'update' or \
                (task['type'] == 'add' and not task['isdir']): # 普通檔案的情況
            local_file = open(task['to_addr'], 'wb')
            ftp.retrbinary('RETR '+task['from_addr'], local_file.write, buffsize) # 寫入本地檔案

        elif task['type'] == 'add' and task['isdir'] : # 要建立資料夾的情況
            os.mkdir(task['to_addr'])

        elif task['type'] == 'del':
            addr = task['addr']
            if os.path.exists(addr):
                if task['isdir']:
                    os.rmdir(addr)
                else:
                    os.remove(addr)

        else:
            raise RuntimeError('Unknown task types')

    ftp.close()

def pull_task_gen(remote_files, local_files):
    tasks = []
    local_left_files = local_files[:]
    remote_rel_list = [x['rel_path'] for x in remote_files]
    local_rel_list = [x['rel_path'] for x in local_files]
    for remote_file in remote_files:
        remote_rel_path = remote_file['rel_path']
        if remote_rel_path in local_rel_list: # 如果本地有該檔案
            local_left_rel_paths = [x['rel_path'] for x in local_left_files]
            local_left_files.pop(local_left_rel_paths.index(remote_rel_path))
            local_file = local_files[local_rel_list.index(remote_rel_path)]
            if local_file['isdir']:
                continue
            if remote_file['mtime'] - local_file['mtime'] > 60 :  # 如果ftp端檔案比本地要提前N秒,則進行同步
                task = dict(
                    type        = 'update',
                    from_addr   = remote_file['abs_path'],
                    to_addr     = local_file['abs_path']
                )
                tasks.append(task)
            else:
                pass
        else:   # 如果本地沒有檔案
            rel_path = remote_file['rel_path']
            rel_path_list = rel_path.split('/')
            rel_path_list = rel_path_list[1:]
            local_abs_path = config.local_dir
            for x in rel_path_list:
                local_abs_path += os.sep + x
            task = dict(
                type        = 'add',
                from_addr   = remote_file['abs_path'],
                to_addr     = local_abs_path,
                isdir       = remote_file['isdir']
            )
            tasks.append(task)

    for left_file in local_left_files[::-1]:
        task = dict(
            type    = 'del',
            addr    = left_file['abs_path'],
            isdir   = left_file['isdir']
        )
        tasks.append(task)
    return tasks

def push():
    print('------------------------------------------------')
    print('start push at {t}\n'.format(t=gen_time()))
    ftp = FTP(config.ftp_ip)
    ftp.encoding = 'utf-8'
    ftp.login(user=config.ftp_user,passwd=config.ftp_pwd)
    remote_files = [x for x in remote_iterator(ftp)]
    local_files = [x for x in local_iterator()]

    tasks = push_task_gen(local_files, remote_files)

    buffsize = 1024
    for task in tasks:
        if task['type'] == 'update' or \
                (task['type'] == 'add' and not task['isdir']): # 普通檔案的情況
            local_file = open(task['from_addr'], 'rb')
            print(task['from_addr'])
            ftp.storbinary('STOR ' + task['to_addr'], local_file, buffsize) #  上傳檔案

        elif task['type'] == 'add' and task['isdir'] : # 要建立資料夾的情況
            ftp.mkd(task['to_addr'])

        elif task['type'] == 'del':
            addr = task['addr']
            if task['isdir']:
                ftp.rmd(addr)
            else:
                print(addr)
                ftp.delete(addr)

    ftp.close()


def push_task_gen(local_files, remote_files):
    tasks = []
    remote_left_files = remote_files[:]
    remote_rel_list = [x['rel_path'] for x in remote_files]
    for local_file in local_files:
        local_rel_path = local_file['rel_path']
        if local_rel_path in remote_rel_list:  # 如果ftp端有該檔案
            remote_left_rel_paths = [x['rel_path'] for x in remote_left_files]
            remote_left_files.pop(remote_left_rel_paths.index(local_rel_path))
            remote_file = remote_files[remote_rel_list.index(local_rel_path)]
            if remote_file['isdir']:
                continue
            if local_file['mtime'] - remote_file['mtime'] > 60:
                task = dict(
                    type        = 'update',
                    from_addr   = local_file['abs_path'],
                    to_addr     = remote_file['abs_path']
                )
                tasks.append(task)
            else:
                pass
        else:   # 如果ftp端沒有檔案
            rel_path = local_file['rel_path']
            rel_path_list = rel_path.split('/')
            rel_path_list = rel_path_list[1:]
            remote_abs_path = config.ftp_dir
            for x in rel_path_list:
                remote_abs_path += '/' + x
            task = dict(
                type        = 'add',
                from_addr   = local_file['abs_path'],
                to_addr     = remote_abs_path,
                isdir       = local_file['isdir']
            )
            tasks.append(task)
    for left_file in remote_left_files[::-1]:
        task = dict(
            type    = 'del',
            addr    = left_file['abs_path'],
            isdir   = left_file['isdir']
        )
        tasks.append(task)
    return tasks

def gen_time():
    t = time.localtime(time.time())
    return time.strftime('%Y-%m-%d %H:%M:%S', t)

# push()
# pull()