Skip to content

Instantly share code, notes, and snippets.

@Gerzer Gerzer/FTP sync.py
Last active Jan 18, 2018

Embed
What would you like to do?
Pythonista FTP sync
import ui
import console
import keychain
import ftplib
import os
import re
import time
from datetime import datetime
global cur_dir
cur_dir = os.path.abspath(os.getcwd())
ftp_files = []
def walkftp(ftp_inst, dirname):
global ftp_files
ftp_inst.cwd(dirname)
for name in ftp_inst.nlst(ftp_inst.pwd()):
path = os.path.join(ftp_inst.pwd(), name)
try:
ftp_inst.cwd(path)
ftp_inst.cwd('..')
ui.in_background(walkftp(ftp_inst, path))
except ftplib.error_perm:
ftp_files.append(path)
def save_handler(sender):
addr_file = open(os.path.join(cur_dir, 'ftp_addr.txt'), 'w')
name_file = open(os.path.join(cur_dir, 'ftp_name.txt'), 'w')
dir_file = open(os.path.join(cur_dir, 'ftp_dir.txt'), 'w')
root_dir_file = open(os.path.join(cur_dir, 'ftp_rootdir.txt'), 'w')
addr_file.write(root['addr'].text)
name_file.write(root['name'].text)
dir_file.write(root['dir'].text)
root_dir_file.write(os.path.abspath(root['rootdir'].text))
addr_file.close()
name_file.close()
dir_file.close()
root_dir_file.close()
keychain.set_password('ftpsync', root['name'].text, root['pswd'].text)
@ui.in_background
def sync_handler(sender):
try:
addr_file = open(os.path.join(cur_dir, 'ftp_addr.txt'), 'r')
name_file = open(os.path.join(cur_dir, 'ftp_name.txt'), 'r')
dir_file = open(os.path.join(cur_dir, 'ftp_dir.txt'), 'r')
root_dir_file = open(os.path.join(cur_dir, 'ftp_rootdir.txt'), 'r')
addr = addr_file.read()
name = name_file.read()
sync_dir = dir_file.read()
root_dir = root_dir_file.read()
addr_file.close()
name_file.close()
dir_file.close()
root_dir_file.close()
pswd = keychain.get_password('ftpsync', name)
except:
console.hud_alert('Couldn\'t find a saved FTP host', 'error', 2.3)
return
console.show_activity('Syncing with FTP server...')
os.chdir(root_dir)
time.sleep(0.35)
# try:
# state_file = open('ftp_state.txt', 'r')
# state = state_file.readlines()
# state_file.close()
# except IOError:
# state_file = open('ftp_state.txt', 'a+')
# state_file.close()
# state = []
if name == '#anonymous#' and pswd == '@anonymous@':
ftp_inst = ftplib.FTP(addr)
ftp_inst.login()
else:
ftp_inst = ftplib.FTP(addr)
ftp_inst.login(name, pswd)
try:
ftp_inst.nlst(ftp_inst.pwd())
ftp_inst.set_debuglevel(1)
walkftp(ftp_inst, sync_dir)
except:
pass
for ftp_file in ftp_files:
ftp_mod_date = datetime.strptime(ftp_inst.sendcmd('MDTM ' + ftp_file)[4:], "%Y%m%d%H%M%S")
local_file = ftp_file.replace(sync_dir + '/', '')[1:]
if os.path.exists(local_file) == True:
local_mod_date = datetime.fromtimestamp(os.path.getmtime(local_file))
if ftp_mod_date > local_mod_date:
ftp_inst.retrbinary('RETR ' + ftp_file, open(local_file, 'wb').write)
else:
pass
else:
head, tail = os.path.split(local_file)
if not os.path.exists(head) and head != '':
os.makedirs(head)
local_file_obj = open(local_file, 'a+')
local_file_obj.close()
ftp_inst.retrbinary('RETR ' + ftp_file, open(local_file, 'wb').write)
for local_dir, local_dirs, local_filenames in os.walk('.'):
if local_dir == './.Trash':
for local_filename in local_filenames:
local_file = os.path.join(local_dir, local_filename)
local_file = local_file.replace('./', '')
ftp_file = '/' + os.path.join(sync_dir, local_file)
if ftp_file in ftp_files:
ftp_inst.delete(ftp_file)
for local_trash_subdir in local_dirs:
local_full_dir = os.path.join(local_dir, local_trash_subdir)
local_full_dir = local_full_dir.replace('./', '')
ftp_dir = '/' + os.path.join(sync_dir, local_full_dir)
try:
ftp_inst.rmd(ftp_dir)
except:
pass
else:
for local_filename in local_filenames:
local_file = os.path.join(local_dir, local_filename)
local_file = local_file.replace('./', '')
ftp_file = '/' + os.path.join(sync_dir, local_file)
if ftp_file in ftp_files:
ftp_mod_date = datetime.strptime(ftp_inst.sendcmd('MDTM ' + ftp_file)[4:], "%Y%m%d%H%M%S")
local_mod_date = datetime.fromtimestamp(os.path.getmtime(local_file))
if local_mod_date > ftp_mod_date:
local_file_obj = open(local_file, 'rb')
head, tail = os.path.split(ftp_file)
if tail.startswith('.') == False:
ftp_inst.storbinary('STOR ' + ftp_file, local_file_obj)
local_file_obj.close()
else:
ftp_inst.cwd('/')
head, tail = os.path.split(ftp_file)
do_store_file = True
for dir_level in head.split('/'):
try:
ftp_inst.cwd(dir_level)
except:
ftp_inst.mkd(dir_level)
ftp_inst.cwd(dir_level)
if do_store_file == True:
local_file_obj = open(local_file, 'rb')
if tail.startswith('.') == False:
ftp_inst.storbinary('STOR ' + ftp_file, local_file_obj)
local_file_obj.close()
ftp_inst.close()
console.hide_activity()
def mode_handler(sender):
if sender.selected_index == 0:
root['rootdir'].enabled = False
root['addr'].enabled = False
root['name'].enabled = False
root['pswd'].enabled = False
root['anon'].enabled = False
root['save'].enabled = False
root['dir'].enabled = False
root['sync'].enabled = True
elif sender.selected_index == 1:
root['rootdir'].enabled = True
root['addr'].enabled = True
if root['anon'].value == False:
root['name'].enabled = True
root['pswd'].enabled = True
root['anon'].enabled = True
root['save'].enabled = True
root['dir'].enabled = True
root['sync'].enabled = False
def anon_handler(sender):
if sender.value == True:
root['name'].text = '#anonymous#'
root['pswd'].text = '@anonymous@'
root['name'].enabled = False
root['pswd'].enabled = False
elif sender.value == False:
root['name'].text = ''
root['pswd'].text = ''
root['name'].enabled = True
root['pswd'].enabled = True
root = ui.load_view()
try:
addr_file = open(os.path.join(cur_dir, 'ftp_addr.txt'), 'r')
name_file = open(os.path.join(cur_dir, 'ftp_name.txt'), 'r')
dir_file = open(os.path.join(cur_dir, 'ftp_dir.txt'), 'r')
root_dir_file = open(os.path.join(cur_dir, 'ftp_rootdir.txt'), 'r')
addr = addr_file.read()
name = name_file.read()
sync_dir = dir_file.read()
root_dir = root_dir_file.read()
addr_file.close()
name_file.close()
dir_file.close()
root_dir_file.close()
pswd = keychain.get_password('ftpsync', name)
except:
addr = ''
name = ''
pswd = ''
root_dir = ''
sync_dir = ''
root['addr'].enabled = False
root['addr'].text = addr
root['name'].enabled = False
root['name'].text = name
root['pswd'].enabled = False
root['pswd'].text = pswd
root['rootdir'].enabled = False
root['rootdir'].text = root_dir
root['anon'].enabled = False
if name == '#anonymous#' and pswd == '@anonymous@':
root['anon'].value = True
root['save'].enabled = False
root['dir'].enabled = False
root['dir'].text = sync_dir
root['mode'].action = mode_handler
root.present('sheet')
[{"class":"View","attributes":{"name":"FTP sync","tint_color":"RGBA(0.000000,0.478000,1.000000,1.000000)","background_color":"RGBA(1.000000,1.000000,1.000000,1.000000)","enabled":true,"border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","flex":""},"frame":"{{0, 0}, {152, 392}}","nodes":[{"class":"TextField","attributes":{"alignment":"left","autocorrection_type":"no","font_size":17,"border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","enabled":true,"flex":"","placeholder":"Server address","text_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","name":"addr","border_style":3,"spellchecking_type":"no","uuid":"D41E71EA-F9BE-4BF4-8A0E-4664A34DA47F"},"frame":"{{8, 38}, {136, 32}}","nodes":[]},{"class":"SegmentedControl","attributes":{"name":"mode","border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","uuid":"F9F1EB6A-6071-48C9-AB58-73FC597764D5","enabled":true,"segments":"Sync|Edit","flex":"LR"},"frame":"{{22, 357}, {120, 29}}","nodes":[]},{"class":"TextField","attributes":{"alignment":"left","autocorrection_type":"no","font_size":17,"border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","enabled":true,"flex":"","placeholder":"Username","text_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","name":"name","border_style":3,"spellchecking_type":"no","uuid":"7D4B39EA-FDC7-4BF8-9060-4301FF746C78"},"frame":"{{8, 78}, {136, 32}}","nodes":[]},{"class":"TextField","attributes":{"alignment":"left","autocorrection_type":"no","font_size":17,"border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","enabled":true,"flex":"","placeholder":"Password","text_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","secure":true,"name":"pswd","border_style":3,"spellchecking_type":"no","uuid":"3903F267-28B5-43E9-920F-2AE569A09F58"},"frame":"{{8, 118}, {136, 32}}","nodes":[]},{"class":"Switch","attributes":{"enabled":true,"flex":"","name":"anon","value":false,"action":"anon_handler","border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","uuid":"C0A89942-B88C-42B1-A451-F8446670E1EB"},"frame":"{{50, 238}, {51, 31}}","nodes":[]},{"class":"Label","attributes":{"font_size":17,"enabled":true,"text":"Anonymous","flex":"","name":"anonlabel","border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","text_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","alignment":"center","uuid":"86E6E1F7-2F72-47C6-9EB0-7366E4F51189"},"frame":"{{8, 198}, {136, 32}}","nodes":[]},{"class":"Button","attributes":{"font_size":15,"enabled":true,"flex":"","font_bold":true,"name":"sync","uuid":"FFA3ADB5-B17D-4E81-9A30-F7F86431D854","border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","action":"sync_handler","image_name":"ionicons-ios7-refresh-32","title":"Sync"},"frame":"{{36, 317}, {80, 32}}","nodes":[]},{"class":"Button","attributes":{"font_size":15,"enabled":true,"flex":"","font_bold":false,"name":"save","uuid":"51D6677C-10AB-46E3-A229-665692F43DB7","border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","action":"save_handler","image_name":"ionicons-ios7-checkmark-32","title":"Save"},"frame":"{{36, 277}, {80, 32}}","nodes":[]},{"class":"TextField","attributes":{"alignment":"left","autocorrection_type":"no","font_size":17,"border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","enabled":true,"flex":"","placeholder":"Sync directory","text_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","name":"dir","border_style":3,"spellchecking_type":"no","uuid":"76FE0AEB-1770-4452-98B1-E84B64CA75B7"},"frame":"{{8, 158}, {136, 32}}","nodes":[]},{"class":"TextField","attributes":{"alignment":"left","autocorrection_type":"no","font_size":17,"border_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","enabled":true,"flex":"","placeholder":"Local root path","text_color":"RGBA(0.000000,0.000000,0.000000,1.000000)","name":"rootdir","border_style":3,"spellchecking_type":"no","uuid":"1C9422C7-A825-4246-B0F3-D8935139A463","secure":false},"frame":"{{8, 0}, {136, 32}}","nodes":[]}]}]
@cclauss

This comment has been minimized.

Copy link

commented Sep 15, 2015

Really cool idea... This could help a LOT!
Issues:

  1. File open() without a matching file close() on lines 85 and 94
  2. Naked except clauses
  3. Why four separate files in save_handler() and sync_handler()?
  4. Else pass clauses are unnecessary and distracting

The following might help to simplify sync_handler():

def get_file_content(data_name):
    with open(os.path.join(cur_dir, 'ftp_{}.txt'.format(data_name))) as in_file:
        return in_file.read()

# ...

data_dict = {k : get_file_content(k) for k in 'addr name dir rootdir'.split()}
@jsbain

This comment has been minimized.

Copy link

commented Sep 16, 2015

Syncing based just of file date can be a little risky, unless this is really intended to be a one way sync. For two way, you maybe need to store the last sync date, locally, in which case if both server and local copies have changed since last sync, prompt user for which version to use, or else rename a copy.

@Gerzer

This comment has been minimized.

Copy link
Owner Author

commented Sep 24, 2015

@cclauss Thanks! Regarding your feedback (which I appreciate a lot):

  1. I just realized that... slaps face
  2. Please excuse my ignorance, I probably know what this is under a different name.
  3. I really should use JSON, Pythonista users could probably figure out the format if they don't know it already.
  4. I am a failure at life... slaps face twice
    Thanks again for the feedback!

@jsbain I guess I expected users to always want the version with the most recent modifications. I could implement conflict handling better, but how much of a need is there? If it is a big problem, I will add that functionality. I just want to get a sense of its value and how people would want conflicts to be resolved, whether it be a manual choice, storing both versions, or what. Thanks!

@cclauss

This comment has been minimized.

Copy link

commented Sep 26, 2015

This is what I meant by naked exceptions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.