Передача нескольких больших файлов с помощью FTPlib (python) через FTPS - PullRequest
0 голосов
/ 23 апреля 2020

после фрагмента кода, что я делаю / пытаюсь сделать в данный момент:

#!/usr/bin/env python3 
# Transfer data from NAS to FTP-Server via FTPS

import sys, os, shutil, math, datetime, hashlib, glob
from io import StringIO
from ftplib import FTP_TLS

def get_file(ftp_connection, filename):
    try:
        ftp_connection.retrbinary("RETR " + filename, open(filename, "wb").write)
    except Exception as e:
        print("ERROR: ", e)

def send_file(ftp_connection, filename):
    try:
        uploadTracker = FtpUploadTracker(int(os.path.getsize(filename)))
        ftp.storbinary("STOR " + filename, open(filename, "rb"), 1024, uploadTracker.handle)
    except Exception as e:
        print("ERROR: ", e)

class FtpUploadTracker:
    sizeWritten = 0
    totalSize = 0
    lastShownPercent = 0

    def __init__(self, totalSize):
        self.totalSize = totalSize

    def handle(self, block):
        self.sizeWritten += 1024
        percentComplete = round((self.sizeWritten / self.totalSize) * 100)
        print('\r  ' + progress_percentage(percentComplete, self.sizeWritten, self.totalSize, width=30), end='')

def convert_size(size_bytes):
   if size_bytes == 0:
       return "0B"
   size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
   i = int(math.floor(math.log(size_bytes, 1024)))
   return "{:0.2f} {}".format(size_bytes / math.pow(1024, i), size_name[i])

def md5(fname):
    hash_md5 = hashlib.md5()
    with open(fname, "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            hash_md5.update(chunk)
    return hash_md5.hexdigest()

def md5_ftp(ftp, remote_path):
    m = hashlib.md5()
    ftp.retrbinary('RETR %s' % remote_path, m.update)
    return m.hexdigest()

def progress_percentage(perc, part, total, width=None):
    # This will only work for python 3.3+ due to use of
    # os.get_terminal_size the print function etc.

    FULL_BLOCK = '█'
    # this is a gradient of incompleteness
    INCOMPLETE_BLOCK_GRAD = ['░', '▒', '▓']

    # assert(isinstance(perc, float))
    assert(0. <= perc <= 100.)
    # if width unset use full terminal
    if width is None:
        width = os.get_terminal_size().columns
    # progress bar is block_widget separator perc_widget : ####### 30%
    max_perc_widget = '[100.00%]' # 100% is max
    separator = ' '
    blocks_widget_width = width - len(separator) - len(max_perc_widget)
    assert(blocks_widget_width >= 10) # not very meaningful if not
    perc_per_block = 100.0/blocks_widget_width
    # epsilon is the sensitivity of rendering a gradient block
    epsilon = 1e-6
    # number of blocks that should be represented as complete
    full_blocks = int((perc + epsilon)/perc_per_block)
    # the rest are "incomplete"
    empty_blocks = blocks_widget_width - full_blocks

    # build blocks widget
    blocks_widget = ([FULL_BLOCK] * full_blocks)
    blocks_widget.extend([INCOMPLETE_BLOCK_GRAD[0]] * empty_blocks)
    # marginal case - remainder due to how granular our blocks are
    remainder = perc - full_blocks*perc_per_block
    # epsilon needed for rounding errors (check would be != 0.)
    # based on reminder modify first empty block shading
    # depending on remainder
    if remainder > epsilon:
        grad_index = int((len(INCOMPLETE_BLOCK_GRAD) * remainder)/perc_per_block)
        blocks_widget[full_blocks] = INCOMPLETE_BLOCK_GRAD[grad_index]

    # build perc widget
    str_perc = '%.2f' % perc
    # -1 because the percentage sign is not included
    perc_widget = '%s %%' % str_perc.ljust(len(max_perc_widget) - 3)
    # already_transfered
    size_state = '(%s of %s)' % (convert_size(part), convert_size(total))

    # form progressbar
    progress_bar = '%s%s%s%s%s     ' % (''.join(blocks_widget), separator, perc_widget, separator, size_state)
    # return progressbar as string
    return ''.join(progress_bar)

def delete_files(folder, fileName):
    # Get a list of all the file paths that match 'filename' pattern from in specified directory
    fileList = glob.glob(os.path.join(folder, fileName))
    # Iterate over the list of filepaths & remove each file.
    for filePath in fileList:
        try:
            os.remove(filePath)
            #print('  - Removed file "{}" in "{}"'.format(os.path.basename(filePath), folder))
            print('  - {}'.format(filePath))
        except:
            print('  - No file "{}" in "{}"'.format(os.path.basename(filePath), folder))

print(" _   _                               ")
print("| | | |                              ")
print("| |_| | ___ _ __ _ __ ___   ___  ___ ")
print("| '_' |/ _ \ '__| '_ ` _ \ / _ \/ __|")
print("| | | |  __/ |  | | | | | |  __/\__ \\")
print("|_| |_|\___|_|  |_| |_| |_|\___||___/")
print("")

files2transfer = []
file2transfer = []

working_dir = os.getcwd()
transfer_from_dir = os.path.join(os.sep, "mnt", "mdottwo", "BackUp")
transfer_to_dir = "/Seagate-Expansion-02/"
dummy_file_size = 100

### DUMMY FILES
print('Dummy files')
# Delete old dummy files in current and source directory
print('  Remove dummy files:')
delete_files(working_dir, 'dummy_*')
delete_files(transfer_from_dir, 'dummy_*')

if len(sys.argv) > 1:
    num_dummy_files = int(sys.argv[1]) + 1
    if len(sys.argv) > 2:
        dummy_file_size = int(sys.argv[2])
    else:
        dummy_file_size = 100
    print("  Generate {} dummy files with a size of {:0.2f} MB ...".format(str(num_dummy_files-1),dummy_file_size),end='')
    sys.stdout.flush()
else:
    num_dummy_files = 1

for i in range(1, num_dummy_files):
    os.system("dd if=/dev/zero of=" + os.path.join(transfer_from_dir, "dummy_"+str(i)+".tia") + "  status=none bs=1M  count="+str(dummy_file_size))
    if i == num_dummy_files - 1:
        print("\r  Generate {} dummy files with a size of {:0.2f} MB: done!               ".format(str(num_dummy_files-1), dummy_file_size, i, num_dummy_files-1))
    else:
        print("\r  Generate {} dummy files with a size of {:0.2f} MB: {} of {} done!".format(str(num_dummy_files-1), dummy_file_size, i, num_dummy_files-1), end='')
    sys.stdout.flush()

print('')
start_time_all = datetime.datetime.now()
print("Start of transfer: {}".format(start_time_all.replace(microsecond=0)))

print("Current working directory: {}".format(working_dir))
#os.chdir(root_dir)
#print("Change working directory to: {}".format(root_dir))

for (root, dirs, files) in os.walk(transfer_from_dir):
    root_dir = root
    for filename in files:
        if filename.endswith(".tia"):
             files2transfer.append(os.path.join(root, filename))
             file2transfer.append(filename)
print("Files to transfer:")
for filename in files2transfer:
    print("  - {}".format(filename))

for current_file in file2transfer:
    print("")
    print("Transfer of file: {}".format(current_file))
    print("  Link to local directory ...", end='')
    os.system("ln -s " + os.path.join(root, current_file) + " " + os.path.join(os.getcwd(), current_file))
    print("\r  Link to local directory: done!")
    with FTP_TLS() as ftp:
        print("  Establishing connction to FTP ...", end='')
        sys.stdout.flush()
        ftp.connect("servername.myfritz.net", 21)
        ftp.login("User", "Password")
        ftp.set_pasv(True)
        ftp.prot_p()
        print("\r  Establishing connction to FTP: done!")

        print("  Set remote directory to: {}".format(transfer_to_dir))
        ftp.cwd(transfer_to_dir)

        start_time_current = datetime.datetime.now()
        print("  Start of transfer: {}".format(start_time_current.replace(microsecond=0)))
        sys.stdout.flush()
        send_file(ftp, current_file)
        end_time_current = datetime.datetime.now()
        print("\n  End of transfer:   {}".format(end_time_current.replace(microsecond=0)))
        print('  Duration of transfer: {}'.format(end_time_current.replace(microsecond=0) - start_time_current.replace(microsecond=0)))
        sys.stdout.flush()
        try:
            remote_file_size = ftp.size(os.path.join(transfer_to_dir, current_file))
            #remote_md5 = md5_ftp(ftp, os.path.join(transfer_to_dir, current_file))
        except:
            remote_file_size = 0
            #remote_md5 = 0
            print("  Remote file does not exist")
        local_file_size = os.path.getsize(current_file)
        print("  File comparison:")
        #print("    - local:  Size: {} ({} B) | MD5: {}".format(convert_size(local_file_size), local_file_size, md5(current_file)))
        #print("    - remote: Size: {} ({} B) | MD5: {}".format(convert_size(remote_file_size), remote_file_size, remote_md5))
        print("    - local:  Size: {} ({} B)".format(convert_size(local_file_size), local_file_size))
        print("    - remote: Size: {} ({} B)".format(convert_size(remote_file_size), remote_file_size))
        if remote_file_size == local_file_size:
            os.remove(current_file)
            print("  Local version of {} removed".format(current_file))
        sys.stdout.flush()
        ftp.close

print("")
end_time_all = datetime.datetime.now()
print("End of transfer: {}".format(end_time_all.replace(microsecond=0)))
print('Duration of whole files transfer: {}'.format(end_time_all.replace(microsecond=0) - start_time_all.replace(microsecond=0)))
print("All files transfers successfully completed!")

Итак, что я в основном делаю (или пытаюсь) - это перенос файлов с жесткого диска, который смонтирован к Raspberry Pi (RasPi, подключенному к маршрутизатору; здесь FritzBox) через FTPS через inte rnet к другому маршрутизатору (также FritzBox), или, точнее, к жесткому диску, установленному на удаленном маршрутизаторе. Или графически:

Жесткий диск -> RasPi (-> Маршрутизатор 1) -> FTPS-Inte rnet -> Маршрутизатор 2 -> Жесткий диск

Показанный выше скрипт работает на RasPi , Теперь шокирующая часть кода выше: он действительно работает! Почему я спрашиваю здесь ... это просто вроде работает ...

Когда я тестирую скрипт с фиктивными файлами (см. Разделы кода, помеченные как "фиктивные"), он отлично работает для одного файла и / или несколько файлов, ЕСЛИ размер файла (ов) достаточно мал. Достаточно маленький, кажется, где-то около 170-200 МБ (метод следа и ошибки относительно размеров файлов). Если размеры файлов становятся больше этого, иногда передается только один файл, иногда несколько (от 2 до 4 файлов) и почти никогда не передаются все файлы (количество тестовых файлов 10-20 файлов). Сценарий просто останавливается, больше не отвечает и может быть только убит или прерван. Поскольку для запланированной задачи меньшее количество больших файлов кажется лучшим выбором, чем более мелкие файлы, это поведение не очень хорошее и его нельзя "обойти", просто используя больше, но меньшие файлы.

Я выполнил это скрипт на нескольких системах (RasPi 2, 2 разных RasPi 4, MacBook) и получал всегда одинаковые ответы. Я также (для целей тестирования) перенесен из локального в другой локальный пункт назначения, чтобы исключить необходимость переноса «за пределы» локальной сети; тот же результат.

Теперь мой вопрос: кто-нибудь знает или имеет представление, ПОЧЕМУ это поведение такое, и КАК его можно решить? Мое предложение было бы, чтобы я реализовал sh* # ошибок и вещи, которые кто-то с большим опытом будет делать лучше. Я очень рад за каждый ввод!

Некоторые дополнительные примечания:

  • Очевидно, что имя сервера, имя пользователя и пароль различаются в «реальном» скрипте.
  • «Настоящий» порт - это более высокий порт (40000+) и внутренне (удаленный маршрутизатор), связанный со стандартным портом FTP из 21.
  • FTPS (не SFTP) используется и должен использоваться (ограничение маршрутизатора /FritzBox).
  • Оба маршрутизатора выполняют принудительное повторное подключение ISP; не каждую ночь хотя бы раз в неделю. Я упоминаю об этом, потому что весь сценарий обанкротится, если это произойдет хотя бы с одним маршрутизатором. Я возился с идеей использовать эту библиотеку (https://pypi.org/project/reconnecting-ftp/), которая переподключает оборванное FTP-соединение ... но она работает только для FTP, а не для FTPS. И я, кажется, не могу перегрузить / переписать эту библиотеку, чтобы также иметь возможность использовать FTPS ... но, по крайней мере, я знаю, что эта библиотека существует.
  • Насколько я могу судить, никаких ограничений относительно размера файла не существует со стороны маршрутизатора.
  • Я упоминаю, что оба маршрутизатора являются FritzBox, потому что это несколько ограничивает меня в отношении уровня контроля ( например, нет возможности использовать SFTP).
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...