после фрагмента кода, что я делаю / пытаюсь сделать в данный момент:
#!/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).