From f60b410124145c932a89435d482d0225a6933ef8 Mon Sep 17 00:00:00 2001 From: kirbylife Date: Sun, 28 Jul 2019 11:18:49 -0500 Subject: [PATCH] Initial commit --- client.py | 359 ++++++++++++++++++++++++++++++++++++++++++++++++ compare_json.py | 49 +++++++ config.py | 43 ++++++ dir_to_json.py | 50 +++++++ first_test.py | 42 ++++++ http_server.py | 67 +++++++++ img/logo.png | Bin 0 -> 42462 bytes second_test.py | 117 ++++++++++++++++ 8 files changed, 727 insertions(+) create mode 100755 client.py create mode 100644 compare_json.py create mode 100644 config.py create mode 100644 dir_to_json.py create mode 100644 first_test.py create mode 100644 http_server.py create mode 100644 img/logo.png create mode 100644 second_test.py diff --git a/client.py b/client.py new file mode 100755 index 0000000..0b901d9 --- /dev/null +++ b/client.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import ftplib +import json +import os +import pathlib +import socket +import sys +from copy import deepcopy +from random import randint +from shutil import rmtree +from threading import Thread +from time import sleep +from tkinter import Button, Entry, Label, StringVar, Tk + +from PIL import Image, ImageTk +from requests import post + +# import rethinkdb as r +from compare_json import compare_json +from dir_to_json import get_json + +# from tcping import Ping + +#ORIGINAL = "/home/kirbylife/Proyectos/munyal_test/another" +ORIGINAL = "/home/kirbylife/Proyectos/munyal_test/" +IP = "localhost" +USERNAME = "munyal" +PASSWORD = "123" +HOSTNAME = socket.gethostname() + str(randint(1, 100000000)) +SKIP_UPLOAD = [] +FLAG_DOWNLOAD = False +pending_routes = [] + + +def check_network(port): + ping = Ping(IP, port, 20) + ping.ping(1) + print(SKIP_UPLOAD) + return ping.result.rows[0].successed == 1 + + +def watch_dir(): + global pending_routes + folder = os.path.join(os.getenv("HOME"), ".munyal") + if not os.path.exists(folder): + pathlib.Path(folder).mkdir(parents=True) + + actual_file = os.path.join(folder, "actual.json") + if not os.path.exists(actual_file): + with open(actual_file, "w") as f: + f.write(json.dumps([])) + pending_file = os.path.join(folder, "pending_routes.json") + if not os.path.exists(pending_file): + with open(pending_file, "w") as f: + f.write(json.dumps([])) + + with open(actual_file, "r") as f: + actual = json.loads(f.read()) + actual = get_json(ORIGINAL) + new = deepcopy(actual) + with open(pending_file, "r") as f: + pending_routes = json.loads(f.read()) + new = get_json(ORIGINAL) + while True: + sleep(0.2) + while True: + try: + jsons = compare_json(deepcopy(actual), deepcopy(new)) + except: + new = get_json(ORIGINAL) + else: + break + changes = get_changes(jsons) + + pending_routes = pending_routes + changes + with open(pending_file, "w") as f: + f.write(json.dumps(pending_routes, indent=4)) + + actual = deepcopy(new) + with open(actual_file, "w") as f: + f.write(json.dumps(actual, indent=4)) + while True: + try: + new = get_json(ORIGINAL) + except: + pass + else: + break + + +def need_deleted(items, route): + out = [] + for item in items: + if item.get("is_file"): + out.append({ + "action": "delete", + "route": os.path.join(route, item.get('name')) + }) + else: + if item.get('content'): + out = out + need_deleted(item.get("content"), + os.path.join(route, item.get('name'))) + else: + out.append({ + "action": "delete_folder", + "route": os.path.join(route, item.get('name')) + }) + return out + + +def need_added(items, route): + out = [] + for item in items: + if item.get("is_file"): + out.append({ + "action": "add", + "route": os.path.join(route, item.get('name')) + }) + else: + if item.get('content'): + out = out + need_added(item.get("content"), + os.path.join(route, item.get('name'))) + else: + out.append({ + "action": "add_folder", + "route": os.path.join(route, item.get('name')) + }) + return out + + +def get_changes(jsons, route=''): + delete, add = jsons + out = need_deleted(delete, route) + need_added(add, route) + return out + + +def _is_ftp_dir(ftp_handle, name): + original_cwd = ftp_handle.pwd() + try: + ftp_handle.cwd(name) + ftp_handle.cwd(original_cwd) + return True + except: + return False + + +def _make_parent_dir(fpath): + #dirname = os.path.dirname(fpath) + dirname = os.path.join(ORIGINAL, fpath) + while not os.path.exists(dirname): + try: + os.makedirs(dirname) + print("created {0}".format(dirname)) + except: + _make_parent_dir(dirname) + + +def _download_ftp_file(ftp_handle, name, dest, overwrite): + if not os.path.exists(dest) or overwrite is True: + try: + with open(dest, 'wb') as f: + ftp_handle.retrbinary("RETR {0}".format(name), f.write) + print("downloaded: {0}".format(dest)) + except FileNotFoundError: + print("FAILED: {0}".format(dest)) + else: + print("already exists: {0}".format(dest)) + + +def _mirror_ftp_dir(ftp_handle, name, overwrite): + for item in ftp_handle.nlst(name): + SKIP_UPLOAD.append(item) + if _is_ftp_dir(ftp_handle, item): + _make_parent_dir(item.lstrip("/")) + _mirror_ftp_dir(ftp_handle, os.path.join(name, item), overwrite) + else: + _download_ftp_file(ftp_handle, item, os.path.join(ORIGINAL, item), + overwrite) + + +def download_ftp_tree(overwrite=False): + FLAG_DOWNLOAD = True + ftp_handle = ftplib.FTP(IP, USERNAME, PASSWORD) + path = "" + original_directory = os.getcwd() + os.chdir(ORIGINAL) + _mirror_ftp_dir(ftp_handle, path, overwrite) + os.chdir(original_directory) + ftp_handle.close() + FLAG_DOWNLOAD = False + + +def upload(*args): + global SKIP_UPLOAD + global pending_routes + print("Modulo de subida listo") + while True: + sleep(0.1) + if check_network('21') and pending_routes: + change = pending_routes.pop(0) + if change['route'] not in SKIP_UPLOAD: + ftp = ftplib.FTP(IP, USERNAME, PASSWORD) + route = os.path.join(ORIGINAL, change['route']) + success = False + while not success: + try: + if change['action'] == 'add': + while FLAG_DOWNLOAD: + print("Wait") + print("Agregar archivo") + with open(route, "rb") as f: + ftp.storbinary("STOR /" + change['route'], f) + elif change['action'] == 'add_folder': + print("Agregar carpeta") + ftp.mkd(change['route']) + elif change['action'] == 'delete': + print("Borrar archivo") + ftp.delete(change['route']) + elif change['action'] == 'delete_folder': + print("Borrar carpeta") + ftp.rmd(change['route']) + else: + print("Unexpected action") + except: + print("Error uploading\n") + r = post("http://" + IP + ':8000/upload', + data={ + 'host': HOSTNAME, + 'action': change['action'], + 'route': change['route'] + }) + r = json.loads(r.text) + print(json.dumps(r, indent=4)) + success = r['status'] == 'ok' + ftp.close() + else: + SKIP_UPLOAD.pop() + return 0 + + +def download(*args): + global SKIP_UPLOAD + while True: + sleep(1) + if check_network(28015) and check_network(21): + try: + download_ftp_tree(overwrite=False) + + print("Modulo de descarga listo") + print("Carpeta " + ORIGINAL) + r.connect(IP, 28015).repl() + cursor = r.table("changes").changes().run() + for document in cursor: + change = document['new_val'] + #print(change) + if change['host'] != HOSTNAME: + route = os.path.join(ORIGINAL, change['route']) + SKIP_UPLOAD.append(change['route']) + try: + if change['action'] == 'add': + print("Agregar archivo") + FLAG_DOWNLOAD = True + ftp = ftplib.FTP(IP, USERNAME, PASSWORD) + with open(route, "wb") as f: + ftp.retrbinary("RETR /" + change['route'], + f.write) + ftp.close() + FLAG_DOWNLOAD = False + elif change['action'] == 'add_folder': + print("Agregar carpeta") + pathlib.Path(route).mkdir(parents=True) + elif change['action'] == 'delete': + print("Borrar archivo") + pathlib.Path(route).unlink() + elif change['action'] == 'delete_folder': + print("Borrar carpeta") + rmtree(route) + else: + print("Unexpected action") + except OSError as e: + print("Error en el sistema operativo") + except: + print("Error en el servidor FTP") + except r.errors.ReqlDriverError as e: + print("Conection refused with rethinkdb") + return 0 + + +def run_client(window, password, username, host, folder): + global PASSWORD + global IP + global USERNAME + global ORIGINAL + + PASSWORD = password.get() + USERNAME = username.get() + IP = host.get() + ORIGINAL = folder.get() + + if not os.path.exists(ORIGINAL): + pathlib.Path(ORIGINAL).mkdir(parents=True) + + download_thread = Thread(target=download, args=[window]) + download_thread.setDaemon(True) + download_thread.start() + + upload_thread = Thread(target=upload, args=[window]) + upload_thread.setDaemon(True) + upload_thread.start() + + watch_dir_thread = Thread(target=watch_dir) + watch_dir_thread.setDaemon(True) + watch_dir_thread.start() + + +def main(args): + root = Tk() + root.geometry("250x400") + + img_logo = Image.open("img/logo.png") + img_tk = ImageTk.PhotoImage(img_logo.resize((200, 150), Image.ANTIALIAS)) + + host = StringVar() + host.set("localhost") + host_field = Entry(root, textvariable=host) + + passwd = StringVar() + passwd_field = Entry(root, textvariable=passwd, show="*") + + folder = StringVar() + folder.set(os.path.join(os.getenv("HOME"), "Munyal")) + folder_field = Entry(root, textvariable=folder) + + connect = Button(root, + text="Conectar", + command=lambda: run_client(root, passwd, host, folder)) + + Label(root, text="MUNYAL").pack() + Label(root, image=img_tk).pack() + Label(root, text="").pack() + Label(root, text="Ruta del servidor").pack() + host_field.pack() + Label(root, text="").pack() + Label(root, text="ContraseƱa").pack() + passwd_field.pack() + Label(root, text="").pack() + Label(root, text="Carpeta a sincronizar").pack() + folder_field.pack() + Label(root, text="").pack() + connect.pack() + + root.mainloop() + + +if __name__ == '__main__': + import sys + sys.exit(main(sys.argv)) diff --git a/compare_json.py b/compare_json.py new file mode 100644 index 0000000..77c1d15 --- /dev/null +++ b/compare_json.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import json + +from dir_to_json import get_json + +def compare_json(json1, json2): + bckup1, bckup2 = json1[:], json2[:] + items1 = list(enumerate(json1)) + items2 = list(enumerate(json2)) + for i, item1 in items1: + for j, item2 in items2: + if item1["name"] == item2["name"]: + if item1["is_file"] == True == item2["is_file"]: + if item1["checksum"] == item2["checksum"]: + json1[i] = None + json2[j] = None + ''' + else: + json1[i]["tag"] = "update" + json2[j] = None + ''' + elif item1["is_file"] == False == item2["is_file"]: + new_json1, new_json2 = compare_json(item1["content"], item2["content"]) + if len(new_json1) == 0: + json1[i] = None + else: + json1[i]["content"] = new_json1 + if len(new_json2) == 0: + json2[j] = None + else: + json2[j]["content"] = new_json2 + elif item1["is_file"] != item2["is_file"]:##### Caso hipotetico imposible ##### + json1[i]["tag"] == "delete" + json1 = list(filter(None, json1)) + json2 = list(filter(None, json2)) + return json1, json2 +if __name__ == "__main__": + try: + json1 = get_json("/home/kirbylife/Proyectos/munyal_test/original") + json2 = get_json("/home/kirbylife/Proyectos/munyal_test/copy") + except: + print("error outside") + json1, json2 = compare_json(json1, json2) + #print(len(json1), len(json2)) + print(json.dumps(json1, indent=4)) + print("\n============\n") + print(json.dumps(json2, indent=4)) diff --git a/config.py b/config.py new file mode 100644 index 0000000..6b7a7ec --- /dev/null +++ b/config.py @@ -0,0 +1,43 @@ +import os +from tkinter import Button, Entry, Label, StringVar, PhotoImage, Tk, Canvas + + +def main(args): + root = Tk() + root.title("Munyal") + root.geometry("320x500") + + logo = PhotoImage(file="img/logo.png") + + host = StringVar() + host_field = Entry(root, textvariable=host, width=30) + + passwd = StringVar() + passwd_field = Entry(root, textvariable=passwd, show="*", width=30) + + folder = StringVar() + folder.set(os.path.join(os.getenv("HOME"), "Munyal")) + folder_field = Entry(root, textvariable=folder, width=30) + + connect = Button(root, text="Conectar", command=lambda: None, width=10) + + Label(root, image=logo).pack() + Label(root, text="MUNYAL", font=("", 30)).pack() + Label(root, text="").pack() + Label(root, text="Nombre del servidor").pack() + host_field.pack() + Label(root, text="").pack() + Label(root, text="ContraseƱa").pack() + passwd_field.pack() + Label(root, text="").pack() + Label(root, text="Carpeta a sincronizar").pack() + folder_field.pack() + Label(root, text="").pack() + connect.pack() + + root.mainloop() + + +if __name__ == '__main__': + import sys + sys.exit(main(sys.argv)) diff --git a/dir_to_json.py b/dir_to_json.py new file mode 100644 index 0000000..5f59b1a --- /dev/null +++ b/dir_to_json.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from hashlib import md5 + +import os +import json + + +def md5sum(filename): + try: + hash = md5() + with open(filename, "rb") as f: + for chunk in iter(lambda: f.read(128 * hash.block_size), b""): + hash.update(chunk) + return hash.hexdigest() + except: + return None + +def get_json(path): + out = [] + items = os.listdir(path) + try: + for item in items: + if item[0] != ".": + item_json = { + "name": item + } + route = os.path.join(path, item) + if os.path.isdir(route): + item_json["is_file"] = False + item_json["content"] = get_json(route) + elif os.path.isfile(route): + item_json["is_file"] = True + item_json["size"] = os.path.getsize(route) + item_json["last_modified"] = os.path.getmtime(route) + item_json["created_at"] = os.path.getctime(route) + checksum = md5sum(route) + if checksum: + item_json["checksum"] = checksum + else: + item = None + out.append(item_json) + except: + return get_json(path) + return out + +if __name__ == "__main__": + output = get_json("/media/kirbylife/DATOS/Proyectos/PyCharmProjects/Munyal/folder_test") + print(json.dumps(output, indent=4)) diff --git a/first_test.py b/first_test.py new file mode 100644 index 0000000..38d97ba --- /dev/null +++ b/first_test.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from compare_json import compare_json +from dir_to_json import get_json + +from copy import deepcopy +from time import sleep + +ORIGINAL = "/home/kirbylife/Proyectos/munyal_test/original" + +def main(args): + actual = get_json(ORIGINAL) + new = deepcopy(actual) + + while True: + delete, add = compare_json(deepcopy(actual), deepcopy(new)) + for item in delete: + if item.get("tag"): + if item.get("tag") == "update": + print("Actualizado el archivo {}".format(item.get("name"))) + else: + print("borrado el archivo {}".format(item.get("name"))) + + for item in add: + print("Agregado el archivo {}".format(item.get("name"))) + actual = deepcopy(new) + new = get_json(ORIGINAL) + return 0 + +if __name__ == '__main__': + import sys + sys.exit(main(sys.argv)) + + + + + + + + + diff --git a/http_server.py b/http_server.py new file mode 100644 index 0000000..fa9ce25 --- /dev/null +++ b/http_server.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from flask import Flask +from flask import request +from flask import jsonify + +import rethinkdb as r + +from random import randint +from time import time + +import json + +app = Flask(__name__) + +def md5sum(filename): + try: + hash = md5() + with open(filename, "rb") as f: + for chunk in iter(lambda: f.read(128 * hash.block_size), b""): + hash.update(chunk) + return hash.hexdigest() + except: + return None + +@app.route("/", methods=["GET"]) +def index(): + return(''' + + + Munyal API + + +

Munyal private API

+ + + ''') + +@app.route("/upload", methods=["POST"]) +def upload(): + try: + r.connect( "localhost", 28015).repl() + cursor = r.table("changes") + + host = request.form.get("host") + action = request.form.get("action") + route = request.form.get("route") + obj = { + 'id' : str(time()).split('.')[0] + str(randint(1, 1000000)), + 'action': action, + 'route': route, + 'host': host + } + status = 'ok' + try: + cursor.insert(obj).run() + except: + status = 'error' + except: + status = 'error' + obj['status'] = status + return jsonify(obj) + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/img/logo.png b/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ab5f196ec4840c996b0ffaaa769535a46b0b35a8 GIT binary patch literal 42462 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

eF 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 HSnT;9VqEC@ literal 0 HcmV?d00001 diff --git a/second_test.py b/second_test.py new file mode 100644 index 0000000..869acdd --- /dev/null +++ b/second_test.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from compare_json import compare_json +from dir_to_json import get_json + +from copy import deepcopy +from time import sleep +from requests import post + +import os +import socket +import json +import ftplib + +def need_deleted(items, route): + out = [] + for item in items: + if item.get("is_file"): + out.append({"action": "delete", "route": os.path.join(route, item.get('name'))}) + else: + if item.get('content'): + out = out + need_deleted(item.get("content"), os.path.join(route, item.get('name'))) + else: + out.append({"action": "delete_folder", "route": os.path.join(route, item.get('name'))}) + return out + +def need_added(items, route): + out = [] + for item in items: + if item.get("is_file"): + out.append({"action": "add", "route": os.path.join(route, item.get('name'))}) + else: + if item.get('content'): + out = out + need_added(item.get("content"), os.path.join(route, item.get('name'))) + else: + out.append({"action": "add_folder", "route": os.path.join(route, item.get('name'))}) + return out + +def get_changes(jsons, route=''): + delete, add = jsons + out = need_deleted(delete, route) + need_added(add, route) + return out + ''' + out = [] + + delete, add = jsons + for item in delete: + if item.get("is_file"): + + if item.get("tag"): + if item.get("tag") == "update": + out.append({"action": "update", "file": os.path.join(route, item.get('name'))}) + elif item.get("tag") == "delete":##### Caso hipotetico imposible ##### + if item.get("is_file"): + out.append({"action": "delete", "file": os.path.join(route, item.get('name'))}) + else: + out.append({"action": "delete_folder", "file": os.path.join(route, item.get('name'))}) + + else: + out.append({"action": "delete", "file": os.path.join(route, item.get('name'))}) + + out.append({"action": "delete", "file": os.path.join(route, item.get('name'))}) + else: + return out + ''' + +ORIGINAL = "/home/kirbylife/Proyectos/munyal_test/original" + +def main(args): + ftp = ftplib.FTP('localhost', 'munyal', '123') + actual = get_json(ORIGINAL) + new = deepcopy(actual) + switch = lambda x,o,d=None: o.get(x) if o.get(x) else d if d else lambda *args: None + while True: + sleep(1) + jsons = compare_json(deepcopy(actual), deepcopy(new)) + changes = get_changes(jsons) + for change in changes: + route = os.path.join(ORIGINAL, change['route']) + success = False + while not success: + # ~ try: + x = change['route'] + if change['action'] == 'add': + print("Agregar archivo") + with open(route, "rb") as f: + ftp.storbinary("STOR /" + x, f) + elif change['action'] == 'add_folder': + print("Agregar carpeta") + ftp.mkd(x) + elif change['action'] == 'delete': + print("Borrar archivo") + ftp.delete(x) + elif change['action'] == 'delete_folder': + print("Borrar carpeta") + ftp.rmd(x) + else: + print("Unexpected action") + r = post('http://127.0.0.1:5000/upload', data={ + 'host': socket.gethostname(), + 'action': change['action'], + 'route': change['route'] + } + ) + r = json.loads(r.text) + print(json.dumps(r, indent=4)) + success = r['status'] == 'ok' + # ~ except: + # ~ print("Error uploading, retrying again\n") + actual = deepcopy(new) + new = get_json(ORIGINAL) + return 0 + +if __name__ == '__main__': + import sys + sys.exit(main(sys.argv))