#!/usr/bin/env python3 # # Copyright (c) Dropbox, Inc. # # dropbox # Dropbox frontend script # This file is part of caja-dropbox @PACKAGE_VERSION@. # # caja-dropbox is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # caja-dropbox is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with caja-dropbox. If not, see . # from __future__ import with_statement import errno import locale import optparse import os import platform import shutil import socket import subprocess import sys import tarfile import tempfile import threading import _thread import time import traceback import urllib.request import gettext try: import gpg gpgme = None except ImportError: gpg = None # Still support gpgme for now. Remove this once we only support 17.04+. try: import gpgme except ImportError: gpgme = None try: gettext.bindtextdomain("@GETTEXT_PACKAGE@","@LOCALEDIR@") gettext.textdomain("@GETTEXT_PACKAGE@") GETTEXT_PACKAGE = "@GETTEXT_PACKAGE@" except: GETTEXT_PACKAGE = "caja-dropbox" _ = gettext.gettext from contextlib import closing, contextmanager from io import BytesIO from operator import methodcaller from os.path import relpath from posixpath import curdir, sep, pardir, join, abspath, commonprefix INFO = _("Dropbox is the easiest way to share and store your files online. Want to learn more? Head to") LINK = "https://www.dropbox.com" WARNING = _("In order to use Dropbox, you must download the proprietary daemon.") GPG_WARNING = _("Note: python-gpg (python-gpgme for Ubuntu 16.10 and lower) is not installed, we will not be able to verify binary signatures.") ERROR_CONNECTING = _("Trouble connecting to Dropbox servers. Maybe your internet connection is down, or you need to set your http_proxy environment variable.") ERROR_SIGNATURE = _("Downloaded binary does not match Dropbox signature, aborting install.") ERROR_INVALID_DROPBOX = _("Could not start the Dropbox daemon. Make sure your computer meets the minimum requirements:\nhttps://www.dropbox.com/help/desktop-web/system-requirements#desktop") DOWNLOAD_LOCATION_FMT = "https://www.dropbox.com/download?plat=%s" SIGNATURE_LOCATION_FMT = "https://www.dropbox.com/download?plat=%s&signature=1" DOWNLOADING = _("Downloading Dropbox... %d%%") UNPACKING = _("Unpacking Dropbox... %d%%") PARENT_DIR = os.path.expanduser("~") DROPBOXD_PATH = "%s/.dropbox-dist/dropboxd" % PARENT_DIR DESKTOP_FILE = "@DESKTOP_FILE_DIR@/caja-dropbox.desktop" enc = locale.getpreferredencoding() # Available from https://linux.dropbox.com/fedora/rpm-public-key.asc DROPBOX_PUBLIC_KEY = b""" -----BEGIN PGP PUBLIC KEY BLOCK----- Version: SKS 1.1.0 mQENBEt0ibEBCACv4hZRPqwtpU6z8+BB5YZU1a3yjEvg2W68+a6hEwxtCa2U++4dzQ+7EqaU q5ybQnwtbDdpFpsOi9x31J+PCpufPUfIG694/0rlEpmzl2GWzY8NqfdBFGGm/SPSSwvKbeNc FMRLu5neo7W9kwvfMbGjHmvUbzBUVpCVKD0OEEf1q/Ii0Qcekx9CMoLvWq7ZwNHEbNnij7ec nvwNlE2MxNsOSJj+hwZGK+tM19kuYGSKw4b5mR8IyThlgiSLIfpSBh1n2KX+TDdk9GR+57TY vlRu6nTPu98P05IlrrCP+KF0hYZYOaMvQs9Rmc09tc/eoQlN0kkaBWw9Rv/dvLVc0aUXABEB AAG0MURyb3Bib3ggQXV0b21hdGljIFNpZ25pbmcgS2V5IDxsaW51eEBkcm9wYm94LmNvbT6J ATYEEwECACAFAkt0ibECGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRD8kYszUESRLi/z B/wMscEa15rS+0mIpsORknD7kawKwyda+LHdtZc0hD/73QGFINR2P23UTol/R4nyAFEuYNsF 0C4IAD6y4pL49eZ72IktPrr4H27Q9eXhNZfJhD7BvQMBx75L0F5gSQwuC7GdYNlwSlCD0AAh Qbi70VBwzeIgITBkMQcJIhLvllYo/AKD7Gv9huy4RLaIoSeofp+2Q0zUHNPl/7zymOqu+5Ox e1ltuJT/kd/8hU+N5WNxJTSaOK0sF1/wWFM6rWd6XQUP03VyNosAevX5tBo++iD1WY2/lFVU JkvAvge2WFk3c6tAwZT/tKxspFy4M/tNbDKeyvr685XKJw9ei6GcOGHD =5rWG -----END PGP PUBLIC KEY BLOCK----- """ def console_print(st="", f=sys.stdout, linebreak=True): f.write(st) if linebreak: f.write(os.linesep) def console_flush(f=sys.stdout): f.flush() def yes_no_question(question): while True: console_print(question, linebreak=False) console_print(" [y/n] ", linebreak=False) console_flush() text = input() if text.lower().startswith("y"): return True elif text.lower().startswith("n"): return False else: console_print("Sorry, I didn't understand that. Please type yes or no.") def plat(): if sys.platform.lower().startswith('linux'): arch = platform.machine() if (arch[0] == 'i' and arch[1].isdigit() and arch[2:4] == '86'): plat = "x86" elif arch == 'x86_64': plat = arch else: FatalVisibleError("Platform not supported") return "lnx.%s" % plat else: FatalVisibleError("Platform not supported") def is_dropbox_running(): pidfile = os.path.expanduser("~/.dropbox/dropbox.pid") try: with open(pidfile, "r") as f: pid = int(f.read()) with open("/proc/%d/cmdline" % pid, "r") as f: cmdline = f.read().lower() except: cmdline = "" return "dropbox" in cmdline @contextmanager def gpg_context(keys): gpg_conf_contents = b'' _gpghome = tempfile.mkdtemp(prefix='tmp.gpghome') try: os.environ['GNUPGHOME'] = _gpghome fp = open(os.path.join(_gpghome, 'gpg.conf'), 'wb') fp.write(gpg_conf_contents) fp.close() if gpg: ctx = gpg.Context() else: ctx = gpgme.Context() loaded = [] for key_file in keys: if gpg: ctx.op_import(key_file.read()) result = ctx.op_import_result() key = ctx.get_key(result.imports[0].fpr) else: result = ctx.import_(key_file) key = ctx.get_key(result.imports[0][0]) loaded.append(key) ctx.signers = loaded yield ctx finally: del os.environ['GNUPGHOME'] shutil.rmtree(_gpghome, ignore_errors=True) class SignatureVerifyError(Exception): pass def verify_signature(key_file, sig_file, plain_file): with gpg_context([key_file]) as ctx: if gpg: ctx.op_verify(sig_file.read(), plain_file.read(), None) result = ctx.op_verify_result() return result.signatures[0].status == 0 # gpgme exists sigs = ctx.verify(sig_file, plain_file, None) return sigs[0].status == None def download_file_chunk(url, buf): opener = urllib.request.build_opener() opener.addheaders = [('User-Agent', "DropboxLinuxDownloader/@PACKAGE_VERSION@")] with closing(opener.open(url)) as f: size = int(f.info()['content-length']) bufsize = int(max(size / 200, 4096)) progress = 0 yield (0, True) while True: try: chunk = f.read(bufsize) progress += len(chunk) buf.write(chunk) yield (float(progress)/size, True) if progress == size: break except OSError as e: if hasattr(e, 'errno') and e.errno == errno.EAGAIN: # nothing left to read yield (float(progress)/size, False) else: raise class DownloadState(object): def __init__(self): self.local_file = BytesIO() def copy_data(self): return download_file_chunk(DOWNLOAD_LOCATION_FMT % plat(), self.local_file) def unpack(self): # download signature signature = BytesIO() for _ in download_file_chunk(SIGNATURE_LOCATION_FMT % plat(), signature): pass signature.seek(0) self.local_file.seek(0) if gpg or gpgme: if not verify_signature(BytesIO(DROPBOX_PUBLIC_KEY), signature, self.local_file): raise SignatureVerifyError() self.local_file.seek(0) archive = tarfile.open(fileobj=self.local_file, mode='r:gz') total_members = len(archive.getmembers()) for i, member in enumerate(archive.getmembers()): filename = os.path.join(PARENT_DIR, member.name) if os.path.exists(filename) and not os.path.isdir(filename): os.unlink(filename) archive.extract(member, PARENT_DIR) yield member.name, i, total_members archive.close() def cancel(self): if not self.local_file.closed: self.local_file.close() def is_dropbox_valid(self): """ Validate that Dropbox runs, so we can show an error message to the user if it doesn't work. Returns True if Dropbox can run, false otherwise. """ f = open("/dev/null", "w") try: a = subprocess.Popen([DROPBOXD_PATH, "/testrun", "0"], preexec_fn=os.setsid, cwd=os.path.expanduser("~"), stderr=sys.stderr, stdout=f, close_fds=True) except Exception as e: print(e) return False # in seconds interval = 0.5 wait_for = 30 for _ in range(int(wait_for / interval)): ret_val = a.poll() if ret_val is None: time.sleep(interval) continue return ret_val == 0 return False def load_serialized_images(): global box_logo_pixbuf, window_icon import gi gi.require_version('GdkPixbuf', '2.0') from gi.repository import GdkPixbuf box_logo_pixbuf = @IMAGEDATA64@ window_icon = @IMAGEDATA16@ GUI_AVAILABLE = os.environ.get("DISPLAY", '') if GUI_AVAILABLE: def download(): import gi gi.require_version('Gdk', '3.0') gi.require_version('Gtk', '3.0') from gi.repository import GObject from gi.repository import Gdk from gi.repository import Gtk from gi.repository import Pango import webbrowser load_serialized_images() global FatalVisibleError def FatalVisibleError(s): error = Gtk.MessageDialog(parent = None, flags = Gtk.DialogFlags.MODAL, type = Gtk.MessageType.ERROR, buttons = Gtk.ButtonsType.OK, message_format = s) error.set_title("Error") error.run() Gtk.main_quit() sys.exit(-1) class GeneratorTask(object): def __init__(self, generator, loop_callback, on_done=None, on_exception=None): self.generator = generator self.loop_callback = loop_callback self.on_done = on_done self.on_exception = on_exception def _run(self, *args, **kwargs): self._stopped = False try: for ret in self.generator(*args, **kwargs): if ret is None: ret = () if not isinstance(ret, tuple): ret = (ret,) GObject.idle_add(self.loop_callback, *ret) if self._stopped: _thread.exit() except Exception as e: print(e) if self.on_exception is not None: GObject.idle_add(self.on_exception, e) else: if self.on_done is not None: GObject.idle_add(self.on_done) def start(self, *args, **kwargs): t = threading.Thread(target=self._run, args=args, kwargs=kwargs) t.setDaemon(True) t.start() def stop(self): self._stopped = True class DownloadDialog(Gtk.Dialog): def handle_delete_event(self, wid, ev, data=None): self.handle_cancel(wid) def handle_dont_show_toggle(self, button, data=None): reroll_autostart(not button.get_active()) def handle_cancel(self, button): if self.task: self.task.stop() if self.download: self.download.cancel() Gtk.main_quit() self.user_cancelled = True def handle_ok(self, button): # begin download self.ok.hide() self.download = DownloadState() self.label.hide() if self.dont_show_again_align is not None: self.dont_show_again_align.hide() self.progress.show() def download_progress(progress, status): if not status: self.task.stop() self.update_progress(DOWNLOADING, progress) def finished(): self.update_progress(DOWNLOADING, 1.0) self.unpack_dropbox() def error(ex): FatalVisibleError(ERROR_CONNECTING) self.update_progress(DOWNLOADING, 0) self.task = GeneratorTask(self.download.copy_data, download_progress, finished, error).start() def update_progress(self, text, fraction): self.progress.set_text(text % int(fraction*100)) self.progress.set_fraction(fraction) def unpack_dropbox(self): def unpack_progress(name, i, total): self.update_progress(UNPACKING, float(i)/total) def finished(): self.update_progress(UNPACKING, 1.0) if not self.download.is_dropbox_valid(): FatalVisibleError(ERROR_INVALID_DROPBOX) Gtk.main_quit() def error(ex): if isinstance(ex, SignatureVerifyError): FatalVisibleError(ERROR_SIGNATURE) else: FatalVisibleError(ERROR_CONNECTING) self.task = GeneratorTask(self.download.unpack, unpack_progress, finished, error).start() def mouse_down(self, widget, event): if self.hovering: self.clicked_link = True def mouse_up(self, widget, event): if self.clicked_link: webbrowser.open(LINK) self.clicked_link = False def label_motion(self, widget, event): offx, offy = self.label.get_layout_offsets() layout = self.label.get_layout() index = layout.xy_to_index(int((offx+event.x)*Pango.SCALE), int((offy+event.y)*Pango.SCALE))[1] link_index = layout.get_text().find(LINK) if index >= link_index and index < link_index+len(LINK): self.hovering = True self.label_box.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2)) else: self.hovering = False self.label_box.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.ARROW)) def __init__(self): super(DownloadDialog, self).__init__(parent = None, title = _("Dropbox Installation")) self.download = None self.hovering = False self.clicked_link = False self.user_cancelled = False self.task = None self.ok = ok = Gtk.Button(stock=Gtk.STOCK_OK) ok.connect('clicked', self.handle_ok) self.action_area.add(ok) ok.show() cancel = Gtk.Button(stock=Gtk.STOCK_CANCEL) cancel.connect('clicked', self.handle_cancel) self.action_area.add(cancel) cancel.show() self.connect('delete_event', self.handle_delete_event) self.box_logo = Gtk.Image.new_from_pixbuf(box_logo_pixbuf) self.box_logo.show() self.set_icon(window_icon) self.progress = Gtk.ProgressBar() self.progress.set_property('width-request', 300) self.progress.set_property('show-text', True) self.label = Gtk.Label() GPG_WARNING_MSG = ("\n\n" + GPG_WARNING) if not gpg and not gpgme else "" self.label.set_markup('%s %s\n\n%s%s' % (INFO, LINK, WARNING, GPG_WARNING_MSG)) self.label.set_line_wrap(True) self.label.set_max_width_chars(42) self.label.set_property('width-request', 300) self.label.show() self.label_box = Gtk.EventBox() self.label_box.add(self.label) self.label_box.connect("button-release-event", self.mouse_up) self.label_box.connect("button-press-event", self.mouse_down) self.label_box.connect("motion-notify-event", self.label_motion) self.label_box.show() def on_realize(widget): self.label_box.add_events(Gdk.EventMask.POINTER_MOTION_MASK) self.label_box.connect("realize", on_realize) self.hbox = Gtk.HBox(spacing=10) self.hbox.set_property('border-width',10) self.hbox.pack_start(self.box_logo, False, False, 0) self.hbox.pack_start(self.label_box, False, False, 0) self.hbox.pack_start(self.progress, False, False, 0) self.hbox.show() self.vbox.add(self.hbox) self.dont_show_again_align = None try: if can_reroll_autostart(): dont_show_again = Gtk.CheckButton.new_with_mnemonic(_("_Don't show this again")) dont_show_again.connect('toggled', self.handle_dont_show_toggle) dont_show_again.show() self.dont_show_again_align = Gtk.Alignment(xalign=1.0, yalign=0.0, xscale=0.0, yscale=0.0) self.dont_show_again_align.add(dont_show_again) self.dont_show_again_align.show() hbox = Gtk.HBox() hbox.set_property('border-width', 10) hbox.pack_start(self.dont_show_again_align, True, True, 0) hbox.show() self.vbox.add(hbox) self.set_resizable(False) except: traceback.print_exc() self.ok.grab_focus() dialog = DownloadDialog() dialog.show() Gtk.main() if dialog.user_cancelled: raise Exception("user cancelled download!!!") else: def download(): global FatalVisibleError def FatalVisibleError(s): console_print("\nError: %s" % s, f=sys.stderr) sys.exit(-1) ESC = "\x1b" save = ESC+"7" unsave = ESC+"8" erase_to_start = ESC+"[1K" write = sys.stdout.write flush = sys.stdout.flush last_progress = [None, None] def setprogress(text, frac): if last_progress == [text, frac]: return if sys.stdout.isatty(): write(erase_to_start) write(unsave) console_print(text % int(100*frac), linebreak=not sys.stdout.isatty()) if sys.stdout.isatty(): flush() last_progress[0], last_progress[1] = text, frac console_print() if sys.stdout.isatty(): write(save) flush() console_print("%s %s\n" % (INFO, LINK)) GPG_WARNING_MSG = ("\n%s" % GPG_WARNING) if not gpg and not gpgme else "" if not yes_no_question("%s%s" % (WARNING, GPG_WARNING_MSG)): return download = DownloadState() try: for progress, status in download.copy_data(): if not status: break setprogress(DOWNLOADING, progress) except Exception: traceback.print_exc() FatalVisibleError(ERROR_CONNECTING) else: setprogress(DOWNLOADING, 1.0) console_print() write(save) try: for _, i, total in download.unpack(): setprogress(UNPACKING, float(i)/total) except SignatureVerifyError: traceback.print_exc() FatalVisibleError(ERROR_SIGNATURE) except Exception: traceback.print_exc() FatalVisibleError(ERROR_CONNECTING) else: setprogress(UNPACKING, 1.0) if not download.is_dropbox_valid(): FatalVisibleError(ERROR_INVALID_DROPBOX) console_print() class CommandTicker(threading.Thread): def __init__(self): threading.Thread.__init__(self) self.stop_event = threading.Event() def stop(self): self.stop_event.set() def run(self): ticks = ['[. ]', '[.. ]', '[...]', '[ ..]', '[ .]', '[ ]'] i = 0 first = True while True: self.stop_event.wait(0.25) if self.stop_event.isSet(): break if i == len(ticks): first = False i = 0 if not first: sys.stderr.write("\r%s\r" % ticks[i]) sys.stderr.flush() i += 1 sys.stderr.flush() class DropboxCommand(object): class CouldntConnectError(Exception): pass class BadConnectionError(Exception): pass class EOFError(Exception): pass class CommandError(Exception): pass def __init__(self, timeout=5): self.s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.s.settimeout(timeout) try: self.s.connect(os.path.expanduser('~/.dropbox/command_socket')) except socket.error: raise DropboxCommand.CouldntConnectError() self.f = self.s.makefile("rw", 4096) def close(self): self.f.close() self.s.close() def __readline(self): try: toret = self.f.readline().rstrip("\n") except socket.error: raise DropboxCommand.BadConnectionError() if toret == '': raise DropboxCommand.EOFError() else: return toret # atttribute doesn't exist, i know what you want def send_command(self, name, args): self.f.write(name) self.f.write("\n") self.f.writelines(("\t".join([k] + ([v] if isinstance(v, str) else list(v))) + "\n") for k,v in args.items()) self.f.write("done\n") self.f.flush() # Start a ticker ticker_thread = CommandTicker() ticker_thread.start() # This is the potentially long-running call. try: ok = self.__readline() == "ok" except KeyboardInterrupt: raise DropboxCommand.BadConnectionError("Keyboard interruption detected") finally: # Tell the ticker to stop. ticker_thread.stop() ticker_thread.join() if ok: toret = {} for i in range(21): if i == 20: raise Exception("close this connection!") line = self.__readline() if line == "done": break argval = line.split("\t") toret[argval[0]] = argval[1:] return toret else: problems = [] for i in range(21): if i == 20: raise Exception("close this connection!") line = self.__readline() if line == "done": break problems.append(line) raise DropboxCommand.CommandError("\n".join(problems)) # this is the hotness, auto marshalling def __getattr__(self, name): try: return super(DropboxCommand, self).__getattr__(name) except: def __spec_command(**kw): return self.send_command(str(name), kw) self.__setattr__(name, __spec_command) return __spec_command commands = {} aliases = {} def command(meth): global commands, aliases assert meth.__doc__, "All commands need properly formatted docstrings (even %r!!)" % meth if hasattr(meth, 'im_func'): # bound method, if we ever have one meth = meth.im_func commands[meth.__name__] = meth meth_aliases = [str(alias) for alias in aliases.keys() if aliases[alias].__name__ == meth.__name__] if meth_aliases: meth.__doc__ += "\nAliases: %s" % ",".join(meth_aliases) return meth def alias(name): def decorator(meth): global commands, aliases assert name not in commands, "This alias is the name of a command." aliases[name] = meth return meth return decorator def requires_dropbox_running(meth): def newmeth(*n, **kw): if is_dropbox_running(): return meth(*n, **kw) else: console_print("Dropbox isn't running!") newmeth.__name__ = meth.__name__ newmeth.__doc__ = meth.__doc__ return newmeth def start_dropbox(): if os.access(DROPBOXD_PATH, os.X_OK): f = open("/dev/null", "w") # we don't reap the child because we're gonna die anyway, let init do it subprocess.Popen([DROPBOXD_PATH], preexec_fn=os.setsid, cwd=os.path.expanduser("~"), stderr=sys.stderr, stdout=f, close_fds=True) # in seconds interval = 0.5 wait_for = 60 for _ in range(int(wait_for / interval)): if is_dropbox_running(): return True # back off from connect for a while time.sleep(interval) return False else: return False # Extracted and modified from os.cmd.Cmd def columnize(list, display_list=None, display_width=None): if not list: console_print("") return non_str = [i for i in range(len(list)) if not (isinstance(list[i], str))] if non_str: raise TypeError("list[i] not a string for i in %s" % ", ".join(map(str, non_str))) if not display_width: d = os.popen('stty size', 'r').read().split() if d: display_width = int(d[1]) else: for item in list: console_print(item) return if not display_list: display_list = list size = len(list) if size == 1: console_print(display_list[0]) return for nrows in range(1, len(list)): ncols = (size+nrows-1) // nrows colwidths = [] totwidth = -2 for col in range(ncols): colwidth = 0 for row in range(nrows): i = row + nrows*col if i >= size: break x = list[i] colwidth = max(colwidth, len(x)) colwidths.append(colwidth) totwidth += colwidth + 2 if totwidth > display_width: break if totwidth <= display_width: break else: nrows = len(list) ncols = 1 colwidths = [0] lines = [] for row in range(nrows): texts = [] display_texts = [] for col in range(ncols): i = row + nrows*col if i >= size: x = "" y = "" else: x = list[i] y = display_list[i] texts.append(x) display_texts.append(y) while texts and not texts[-1]: del texts[-1] original_texts = texts[:] for col in range(len(texts)): texts[col] = texts[col].ljust(colwidths[col]) texts[col] = texts[col].replace(original_texts[col], display_texts[col]) line = " ".join(texts) lines.append(line) for line in lines: console_print(line) @command def update(args): """download latest version of dropbox dropbox update Downloads the latest version of dropbox. """ download() @command @requires_dropbox_running @alias('stat') def filestatus(args): """get current sync status of one or more files dropbox filestatus [-l] [-a] [FILE]... Prints the current status of each FILE. options: -l --list prints out information in a format similar to ls. works best when your console supports color :) -a --all do not ignore entries starting with . """ global enc oparser = optparse.OptionParser() oparser.add_option("-l", "--list", action="store_true", dest="list") oparser.add_option("-a", "--all", action="store_true", dest="all") (options, args) = oparser.parse_args(args) try: with closing(DropboxCommand()) as dc: if options.list: # Listing. # Separate directories from files. if len(args) == 0: dirs, nondirs = ["."], [] else: dirs, nondirs = [], [] for a in args: try: (dirs if os.path.isdir(a) else nondirs).append(a) except UnicodeDecodeError: continue if len(dirs) == 0 and len(nondirs) == 0: #TODO: why? exit(1) dirs.sort(key=methodcaller('lower')) nondirs.sort(key=methodcaller('lower')) # Gets a string representation for a path. def path_to_string(file_path): if not os.path.exists(file_path): path = "%s (File doesn't exist!)" % os.path.basename(file_path) return (path, path) try: status = dc.icon_overlay_file_status(path=file_path).get('status', [None])[0] except DropboxCommand.CommandError as e: path = "%s (%s)" % (os.path.basename(file_path), e) return (path, path) env_term = os.environ.get('TERM','') supports_color = (sys.stderr.isatty() and ( env_term.startswith('vt') or env_term.startswith('linux') or 'xterm' in env_term or 'color' in env_term ) ) # TODO: Test when you don't support color. if not supports_color: path = os.path.basename(file_path) return (path, path) if status == "up to date": init, cleanup = "\x1b[32;1m", "\x1b[0m" elif status == "syncing": init, cleanup = "\x1b[36;1m", "\x1b[0m" elif status == "unsyncable": init, cleanup = "\x1b[41;1m", "\x1b[0m" elif status == "selsync": init, cleanup = "\x1b[37;1m", "\x1b[0m" else: init, cleanup = '', '' path = os.path.basename(file_path) return (path, "%s%s%s" % (init, path, cleanup)) # Prints a directory. def print_directory(name): clean_paths = [] formatted_paths = [] for subname in sorted(os.listdir(name), key=methodcaller('lower')): if type(subname) != str: continue if not options.all and subname[0] == '.': continue try: clean, formatted = path_to_string(os.path.abspath(os.path.join(name, subname))) clean_paths.append(clean) formatted_paths.append(formatted) except (UnicodeEncodeError, UnicodeDecodeError): continue columnize(clean_paths, formatted_paths) try: if len(dirs) == 1 and len(nondirs) == 0: print_directory(dirs[0]) else: nondir_formatted_paths = [] nondir_clean_paths = [] for name in nondirs: try: clean, formatted = path_to_string(os.path.abspath(name)) nondir_clean_paths.append(clean) nondir_formatted_paths.append(formatted) except (UnicodeEncodeError, UnicodeDecodeError): continue if nondir_clean_paths: columnize(nondir_clean_paths, nondir_formatted_paths) if len(nondirs) == 0: console_print(dirs[0] + ":") print_directory(dirs[0]) dirs = dirs[1:] for name in dirs: console_print() console_print(name + ":") print_directory(name) except DropboxCommand.EOFError: console_print("Dropbox daemon stopped.") except DropboxCommand.BadConnectionError: console_print("Dropbox isn't responding!") else: if len(args) == 0: args = [name for name in sorted(os.listdir("."), key=methodcaller('lower')) if type(name) == str] if len(args) == 0: # Bail early if there's nothing to list to avoid crashing on indent below console_print("") return indent = max(len(st)+1 for st in args) for file in args: try: if type(file) is not str: file = file.decode(enc) fp = os.path.abspath(file) except (UnicodeEncodeError, UnicodeDecodeError): continue if not os.path.exists(fp): console_print("%-*s %s" % \ (indent, file+':', "File doesn't exist")) continue try: status = dc.icon_overlay_file_status(path=fp).get('status', ['unknown'])[0] console_print("%-*s %s" % (indent, file+':', status)) except DropboxCommand.CommandError as e: console_print("%-*s %s" % (indent, file+':', e)) except DropboxCommand.CouldntConnectError: console_print("Dropbox isn't running!") @command @requires_dropbox_running def ls(args): """list directory contents with current sync status dropbox ls [FILE]... This is an alias for filestatus -l """ return filestatus(["-l"] + args) @command @requires_dropbox_running def puburl(args): """get public url of a file in your dropbox's public folder dropbox puburl FILE Prints out a public url for FILE (which must be in your public folder). """ if len(args) != 1: console_print(puburl.__doc__,linebreak=False) return try: with closing(DropboxCommand()) as dc: try: console_print(dc.get_public_link(path=os.path.abspath(args[0])).get('link', ['No Link'])[0]) except DropboxCommand.CommandError as e: console_print("Couldn't get public url: " + str(e)) except DropboxCommand.BadConnectionError: console_print("Dropbox isn't responding!") except DropboxCommand.EOFError: console_print("Dropbox daemon stopped.") except DropboxCommand.CouldntConnectError: console_print("Dropbox isn't running!") @command @requires_dropbox_running def sharelink(args): """get a shared link for a file in your dropbox dropbox sharelink FILE Prints out a shared link for FILE. """ if len(args) != 1: console_print(sharelink.__doc__, linebreak=False) return try: with closing(DropboxCommand()) as dc: try: path = os.path.abspath(args[0]) link = dc.get_shared_link(path=path).get('link', ['No link'])[0] console_print(link) except DropboxCommand.CommandError as e: console_print("Couldn't get shared link: " + str(e)) except DropboxCommand.BadConnectionError: console_print("Dropbox isn't responding!") except DropboxCommand.EOFError: console_print("Dropbox daemon stopped.") except DropboxCommand.CouldntConnectError: console_print("Dropbox isn't running!") @command @requires_dropbox_running def proxy(args): """set proxy settings for Dropbox dropbox proxy MODE [TYPE] [HOST] [PORT] [USERNAME] [PASSWORD] Set proxy settings for Dropbox. MODE - one of "none", "auto", "manual" TYPE - one of "http", "socks4", "socks5" (only valid with "manual" mode) HOST - proxy hostname (only valid with "manual" mode) PORT - proxy port (only valid with "manual" mode) USERNAME - (optional) proxy username (only valid with "manual" mode) PASSWORD - (optional) proxy password (only valid with "manual" mode) """ mode = None type_ = None if len(args) >= 1: mode = args[0].lower() if len(args) >= 2: type_ = args[1].lower() if (len(args) == 0 or mode not in ['none', 'auto', 'manual'] or (mode == 'manual' and len(args) not in (4, 6)) or (mode != 'manual' and len(args) != 1) or (mode == 'manual' and type_ not in ['http', 'socks4', 'socks5'])): # Print help console_print(proxy.__doc__, linebreak=False) return ARGS = ['mode', 'type', 'host', 'port', 'username', 'password'] # Load the args into a dictionary kwargs = dict(zip(ARGS, args)) # Re-set these two because they were coerced to lower case kwargs['mode'] = mode if type_: kwargs['type'] = type_ try: with closing(DropboxCommand()) as dc: try: dc.set_proxy_settings(**kwargs) console_print('set') except DropboxCommand.CommandError as e: console_print("Couldn't set proxy: " + str(e)) except DropboxCommand.BadConnectionError: console_print("Dropbox isn't responding!") except DropboxCommand.EOFError: console_print("Dropbox daemon stopped.") except DropboxCommand.CouldntConnectError: console_print("Dropbox isn't running!") @command @requires_dropbox_running def throttle(args): """set bandwidth limits for Dropbox dropbox throttle DOWNLOAD UPLOAD Set bandwidth limits for file sync. DOWNLOAD - either "unlimited" or a manual limit in KB/s UPLOAD - one of "unlimited", "auto", or a manual limit in KB/s """ if len(args) != 2: console_print(throttle.__doc__, linebreak=False) return downlimit = args[0].lower() uplimit = args[1].lower() download_limit = None download_mode = None if downlimit == 'unlimited': download_mode = downlimit else: try: download_limit = int(downlimit) download_mode = 'manual' except ValueError: console_print(throttle.__doc__, linebreak=False) return upload_limit = None upload_mode = None if uplimit in ['unlimited', 'auto']: upload_mode = uplimit else: try: upload_limit = int(uplimit) upload_mode = 'manual' except ValueError: console_print(throttle.__doc__, linebreak=False) return kwargs = { 'download_mode': download_mode, 'upload_mode': upload_mode, } if download_limit: kwargs['download_limit'] = str(download_limit) if upload_limit: kwargs['upload_limit'] = str(upload_limit) try: with closing(DropboxCommand()) as dc: try: dc.set_bandwidth_limits(**kwargs) console_print('set') except DropboxCommand.CommandError as e: console_print("Couldn't set bandwidth limits: " + str(e)) except DropboxCommand.BadConnectionError: console_print("Dropbox isn't responding!") except DropboxCommand.EOFError: console_print("Dropbox daemon stopped.") except DropboxCommand.CouldntConnectError: console_print("Dropbox isn't running!") @command @requires_dropbox_running def status(args): """get current status of the dropboxd dropbox status Prints out the current status of the Dropbox daemon. """ if len(args) != 0: console_print(status.__doc__,linebreak=False) return try: with closing(DropboxCommand()) as dc: try: lines = dc.get_dropbox_status()['status'] if len(lines) == 0: console_print('Idle') else: for line in lines: console_print(line) grab_link_url_if_necessary() except KeyError: console_print("Couldn't get status: daemon isn't responding") except DropboxCommand.CommandError as e: console_print("Couldn't get status: " + str(e)) except DropboxCommand.BadConnectionError: console_print("Dropbox isn't responding!") except DropboxCommand.EOFError: console_print("Dropbox daemon stopped.") except DropboxCommand.CouldntConnectError: console_print("Dropbox isn't running!") @command def running(argv): """return whether dropbox is running dropbox running Returns 1 if running 0 if not running. """ return int(is_dropbox_running()) @command @requires_dropbox_running def stop(args): """stop dropboxd dropbox stop Stops the dropbox daemon. """ try: with closing(DropboxCommand()) as dc: try: dc.tray_action_hard_exit() except DropboxCommand.BadConnectionError: console_print("Dropbox isn't responding!") except DropboxCommand.EOFError: console_print("Dropbox daemon stopped.") except DropboxCommand.CouldntConnectError: console_print("Dropbox isn't running!") #returns true if link is necessary def grab_link_url_if_necessary(): try: with closing(DropboxCommand()) as dc: try: link_url = dc.needs_link().get("link_url", None) if link_url is not None: console_print("To link this computer to a dropbox account, visit the following url:\n%s" % link_url[0]) return True else: return False except DropboxCommand.CommandError: pass except DropboxCommand.BadConnectionError: console_print("Dropbox isn't responding!") except DropboxCommand.EOFError: console_print("Dropbox daemon stopped.") except DropboxCommand.CouldntConnectError: console_print("Dropbox isn't running!") @command @requires_dropbox_running def lansync(argv): """enables or disables LAN sync dropbox lansync [y/n] options: y dropbox will use LAN sync (default) n dropbox will not use LAN sync """ if len(argv) != 1: console_print(lansync.__doc__, linebreak=False) return s = argv[0].lower() if s.startswith('y') or s.startswith('-y'): should_lansync = True elif s.startswith('n') or s.startswith('-n'): should_lansync = False else: should_lansync = None if should_lansync is None: console_print(lansync.__doc__,linebreak=False) else: with closing(DropboxCommand()) as dc: dc.set_lan_sync(lansync='enabled' if should_lansync else 'disabled') @command @requires_dropbox_running def exclude(args): """ignores/excludes a directory from syncing dropbox exclude [list] dropbox exclude add [DIRECTORY] [DIRECTORY] ... dropbox exclude remove [DIRECTORY] [DIRECTORY] ... "list" prints a list of directories currently excluded from syncing. "add" adds one or more directories to the exclusion list, then resynchronizes Dropbox. "remove" removes one or more directories from the exclusion list, then resynchronizes Dropbox. With no arguments, executes "list". Any specified path must be within Dropbox. """ if len(args) == 0: try: with closing(DropboxCommand()) as dc: try: lines = [relpath(path) for path in dc.get_ignore_set()['ignore_set']] lines.sort() if len(lines) == 0: console_print('No directories are being ignored.') else: console_print('Excluded: ') for line in lines: console_print(str(line)) except KeyError: console_print("Couldn't get ignore set: daemon isn't responding") except DropboxCommand.CommandError as e: if e.args[0].startswith("No command exists by that name"): console_print("This version of the client does not support this command.") else: console_print("Couldn't get ignore set: " + str(e)) except DropboxCommand.BadConnectionError: console_print("Dropbox isn't responding!") except DropboxCommand.EOFError: console_print("Dropbox daemon stopped.") except DropboxCommand.CouldntConnectError: console_print("Dropbox isn't running!") elif len(args) == 1 and args[0] == "list": exclude([]) elif len(args) >= 2: sub_command = args[0] paths = args[1:] absolute_paths = [os.path.abspath(path) for path in paths] if sub_command == "add": try: with closing(DropboxCommand(timeout=None)) as dc: try: result = dc.ignore_set_add(paths=absolute_paths) if result["ignored"]: console_print("Excluded: ") lines = [relpath(path) for path in result["ignored"]] for line in lines: console_print(str(line)) except KeyError: console_print("Couldn't add ignore path: daemon isn't responding") except DropboxCommand.CommandError as e: if e.args[0].startswith("No command exists by that name"): console_print("This version of the client does not support this command.") else: console_print("Couldn't get ignore set: " + str(e)) except DropboxCommand.BadConnectionError as e: console_print("Dropbox isn't responding! [%s]" % e) except DropboxCommand.EOFError: console_print("Dropbox daemon stopped.") except DropboxCommand.CouldntConnectError: console_print("Dropbox isn't running!") elif sub_command == "remove": try: with closing(DropboxCommand(timeout=None)) as dc: try: result = dc.ignore_set_remove(paths=absolute_paths) if result["removed"]: console_print("No longer excluded: ") lines = [relpath(path) for path in result["removed"]] for line in lines: console_print(str(line)) except KeyError: console_print("Couldn't remove ignore path: daemon isn't responding") except DropboxCommand.CommandError as e: if e.args[0].startswith("No command exists by that name"): console_print("This version of the client does not support this command.") else: console_print("Couldn't get ignore set: " + str(e)) except DropboxCommand.BadConnectionError as e: console_print("Dropbox isn't responding! [%s]" % e) except DropboxCommand.EOFError: console_print("Dropbox daemon stopped.") except DropboxCommand.CouldntConnectError: console_print("Dropbox isn't running!") else: console_print(exclude.__doc__, linebreak=False) return else: console_print(exclude.__doc__, linebreak=False) return @command def start(argv): """start dropboxd dropbox start [-i] Starts the dropbox daemon, dropboxd. If dropboxd is already running, this will do nothing. options: -i --install auto install dropboxd if not available on the system """ should_install = "-i" in argv or "--install" in argv # first check if dropbox is already running if is_dropbox_running(): if not grab_link_url_if_necessary(): console_print("Dropbox is already running!") return console_print("Starting Dropbox...", linebreak=False) console_flush() if not start_dropbox(): if not should_install: console_print() console_print("The Dropbox daemon is not installed!") console_print("Run \"dropbox start -i\" to install the daemon") return # install dropbox!!! try: download() except: traceback.print_exc() else: if GUI_AVAILABLE: start_dropbox() console_print("Done!") # downloaded dropbox doesn't create autostart file, do it ourselves reroll_autostart(True) else: if start_dropbox(): if not grab_link_url_if_necessary(): console_print("Done!") else: if not grab_link_url_if_necessary(): console_print("Done!") def can_reroll_autostart(): return ".config" in os.listdir(os.path.expanduser('~')) def reroll_autostart(should_autostart): home_dir = os.path.expanduser('~') contents = os.listdir(home_dir) # UBUNTU if ".config" in contents: autostart_dir = os.path.join(home_dir, ".config", "autostart") autostart_link = os.path.join(autostart_dir, "caja-dropbox.desktop") if should_autostart: if os.path.exists(DESKTOP_FILE): if not os.path.exists(autostart_dir): os.makedirs(autostart_dir) shutil.copyfile(DESKTOP_FILE, autostart_link) elif os.path.exists(autostart_link): os.remove(autostart_link) @command def autostart(argv): """automatically start dropbox at login dropbox autostart [y/n] options: n dropbox will not start automatically at login y dropbox will start automatically at login (default) Note: May only work on current Ubuntu distributions. """ if len(argv) != 1: console_print(''.join(autostart.__doc__.split('\n', 1)[1:])) return s = argv[0].lower() if s.startswith('y') or s.startswith('-y'): should_autostart = True elif s.startswith('n') or s.startswith('-n'): should_autostart = False else: should_autostart = None if should_autostart is None: console_print(autostart.__doc__,linebreak=False) else: reroll_autostart(should_autostart) @command def help(argv): """provide help dropbox help [COMMAND] With no arguments, print a list of commands and a short description of each. With a command, print descriptive help on how to use the command. """ if not argv: return usage(argv) for command in commands: if command == argv[0]: console_print(commands[command].__doc__.split('\n', 1)[1]) return for alias in aliases: if alias == argv[0]: console_print(aliases[alias].__doc__.split('\n', 1)[1]) return console_print("unknown command '%s'" % argv[0], f=sys.stderr) def usage(argv): console_print("Dropbox command-line interface\n") console_print("commands:\n") console_print("Note: use dropbox help to view usage for a specific command.\n") out = [] for command in commands: out.append((command, commands[command].__doc__.splitlines()[0])) spacing = max(len(o[0])+3 for o in out) for o in out: console_print(" %-*s%s" % (spacing, o[0], o[1])) console_print() def main(argv): global commands # now we need to find out if one of the commands are in the # argv list, and if so split the list at the point to # separate the argv list at that point cut = None for i in range(len(argv)): if argv[i] in commands or argv[i] in aliases: cut = i break if cut == None: usage(argv) os._exit(0) return # lol no options for now globaloptionparser = optparse.OptionParser() globaloptionparser.parse_args(argv[0:i]) # now dispatch and run result = None if argv[i] in commands: result = commands[argv[i]](argv[i+1:]) elif argv[i] in aliases: result = aliases[argv[i]](argv[i+1:]) # flush, in case output is rerouted to a file. console_flush() # done return result if __name__ == "__main__": ret = main(sys.argv) if ret is not None: sys.exit(ret)