def file_type(self, remote_file): assert not crypto.is_encrypted_path(remote_file), \ "file_type receives unencrypted paths" # Try finding plain file first if remote_file[-1] == '/': response = self.client.file_type(remote_file[:-1]) else: response = self.client.file_type(remote_file) is_encrypted = False if response['content'] is None and response['status'] == 'online': # Then encrypted file if remote_file[-1] == '/': remote_file_hash = crypto.get_path_hash(remote_file[:-1]) else: remote_file_hash = crypto.get_path_hash(remote_file) response = self.client.file_type(remote_file_hash) is_encrypted = True if response['content'] is None: is_encrypted = False if response['status'] != 'online': raise VimboxOfflineError("Connection error") if response['content'] == 'dir': assert remote_file[-1] == '/', \ VimboxClientError("Folder paths must end in /") elif response['content'] == 'file': assert remote_file[-1] != '/', \ VimboxClientError("File paths can not end in /") return response['content'], is_encrypted, response['status']
def remove(self, remote_file, recursive=False, password=None): if remote_file == '/': raise VimboxClientError( "\nRemoving root is disallowed, just in case you have fat" " fingers\n") # Extra check for deletable files/folders is_rem, reason, is_encrypted = \ self.is_removable(remote_file, recursive=recursive) if not is_rem: raise VimboxClientError("\nCan not remove due to: %s\n" % reason) # Hash name if necessary if is_encrypted: original_name = remote_file remote_file = crypto.get_path_hash(remote_file) else: original_name = remote_file # TODO: This should go to the client specific part and have exception # handling if remote_file[-1] == '/': # Remove backslash # TODO: This is input sanity check should go in the client # dependent part response = self.client.files_delete(remote_file[:-1]) # update cache # local.update_cache() else: response = self.client.files_delete(remote_file) if response['status'] == 'api-error': # Remove local copy local_file = self.get_local_file(remote_file) if os.path.isfile(local_file): os.remove(local_file) elif os.path.isdir(local_file): shutil.rmtree(local_file) self.unregister_file(remote_file) if self.verbose > 0: print("%s did not exist in remote!" % original_name) elif response['status'] != 'connection-error': if self.verbose > 0: print("%-12s %s" % (yellow("removed"), original_name)) # Remove local copy local_file = self.get_local_file(remote_file) if os.path.isfile(local_file): os.remove(local_file) elif os.path.isdir(local_file): shutil.rmtree(local_file) self.unregister_file(original_name) elif self.verbose > 0: print("%-12s did not remove! %s" % (red("offline"), original_name))
def _tentative_fetch(self, remote_file, password): # Initial assumption about encryption if password: is_encrypted = True remote_file_hash = crypto.get_path_hash(remote_file) response = self.client.file_download(remote_file_hash) else: is_encrypted = False response = self.client.file_download(remote_file) if response['status'] == 'api-error': # Unexpected error raise VimboxClientError("api-error:\n%s" % response['alerts']) elif response['status'] == 'connection-error': # Offline raise VimboxOfflineError("Connection error") elif response['content'] is None: # No file found, but need to check for hashed / unhashed name # collision if password: # We asked for an encrypted file, try unencrypted response = self.client.file_download(remote_file) else: # We asked for an unencrypted file try encrypted remote_file_hash = crypto.get_path_hash(remote_file) response = self.client.file_download(remote_file_hash) # Second check if response['status'] == 'api-error': # Unexpected error # This one is weird, since we tried once and it was ok raise VimboxClientError("api-error:\n%s" % response['alerts']) elif response['status'] == 'connection-error': # Suddenly, Offline raise VimboxOfflineError("Connection error") elif response['content'] is None: # File really does not exist pass elif password: # Provided a password but the file exists unencrypted in remote VimboxClientError( "Tried to fetch encrypted version of %s but it exists " "unencrypted in remote" % remote_file) else: # Tried to fetch as unecrypted but it is encrypted is_encrypted = True return response, is_encrypted
def password_prompt(remote_file, config): # Check if file in cache already if remote_file in config['path_hashes'].values(): VimboxClientError('\nCan not re-encrypt a registered file.\n') # Prompt for password password = getpass.getpass('Input file password: '******'Repeat file password: ') if not password: VimboxClientError("Passwords can not be empty!") if password != password2: VimboxClientError("Passwords do not match!") return password
def is_removable(self, remote_file, recursive=False): """Check if file/folder is removable""" # Disallow deleting of encrypted files that have unknown name. Also # consider the unfrequent file is registered but user uses hash name # Disallow deleting of folders. file_type, is_encrypted, status = self.file_type(remote_file) if status != 'online': raise VimboxOfflineError("Connection error") elif file_type == 'dir': if recursive: is_rem = True reason = None else: is_rem = False reason = "Need to use recursive flag -R to remove folders" elif (file_type == 'file' and remote_file not in self.config['path_hashes'].values() and is_encrypted): is_rem = False reason = "Can not delete uncached encrypted files" elif file_type == 'file' or remote_file in self.config['cache']: is_rem = True reason = None else: # A file may not exist but be on cache (invalid state) allow # deleting in this case raise VimboxClientError("%s does not exist in remote" % remote_file) return is_rem, reason, is_encrypted
def pull(self, remote_file, force_creation, password=None, automerge_rules=None, amerge_ref_is_local=False): if force_creation: # Check created files/folders with same name in cache and remote if remote_file in self.config['path_hashes'].values(): raise VimboxClientError( '\n%s exists in remote and is encrypted.\n' % remote_file) file_type, is_encripted, fetch_status = self.file_type(remote_file) if file_type == 'file': message = '\n%s exists in remote.\n' % remote_file if is_encripted: message += " and is encrypted" raise VimboxClientError(message) elif file_type == 'dir': message = '\n%s exists in remote as a folder.\n' % remote_file raise VimboxClientError(message) content = {'local': None, 'remote': None, 'merged': None} else: # Fetch remote content for this file. If there is connction error, # use offline mode response, password = self.fetch(remote_file, password=password) # Force use of -f or -e to create new folders if (response['status'] == 'online' and response['content'] is None and not force_creation # and # not local_content ): raise VimboxClientError( 'You need to create a file, use -f or -e') # Merge if response['status'] == 'online': content = self.merge(remote_file, response['content'], automerge_rules, amerge_ref_is_local) else: raise VimboxOfflineError("Connection error") return content, 'online', password
def move(self, remote_source, remote_target): """Copy and remove""" is_rem, reason, is_encrypted = self.is_removable(remote_source) recursive = False # FIXME: Capturing a string is brittle if reason == 'Need to use recursive flag -R to remove folders': is_rem = True recursive = True if not is_rem: raise VimboxClientError("Can not move (remove) due to: %s" % reason) self.copy(remote_source, remote_target) self.remove(remote_source, recursive=recursive)
def make_directory(self, remote_target): if remote_target[-1] != '/': raise VimboxClientError("Folder paths must end in / ") file_type, is_encripted, status = self.file_type(remote_target) if status != 'online': raise VimboxOfflineError("Connection error") if file_type is None: response = self.client.make_directory(remote_target[:-1]) if response['status'] == 'online': # Local file os.mkdir(local.get_local_file(remote_target)) # Cache self.register_file(remote_target, False) elif file_type == 'dir': raise VimboxClientError("%s already exists" % remote_target) elif is_encripted: raise VimboxClientError("%s already exists as an encrypted file" % remote_target) else: raise VimboxClientError("%s already exists as a file" % remote_target) return {'status': status, 'content': None, 'alert': None}
def fetch(self, remote_file, password=None): """ Get local and remote content and coresponding file paths """ # Name of the remote file assert remote_file[0] == '/', "Dropbox remote paths start with /" assert remote_file[-1] != '/', "Can only fetch files" # Fetch file without assumptions about encryption response, is_encrypted = self._tentative_fetch(remote_file, password) # Decryption if response['content'] and is_encrypted: if not password: password = getpass.getpass('Input file password: '******'content'], sucess = crypto.decript_content( response['content'], validated_password) if not sucess: raise VimboxClientError("Decrypting %s filed" % remote_file) return response, password
def argument_handling(args): # Edit / ls alias remote_file = None force_creation = False encrypt = False initial_text = None for option in args: if option == '-f': # Create new file force_creation = True elif option == '-e': # Create new encrypted file force_creation = True encrypt = True elif option[0] == '/' or DOC_REGEX.match(option): assert not remote_file, \ "Only one file path can be edited at a time" # Dropbox path remote_file = option elif force_creation and remote_file: # If there is an extra argument not matching the previous and we # are in creation mode, admit this is as initial text initial_text = option else: return None # Sanity checks # Check we got a file path if remote_file is None: return None # Quick exit: edit file is a folder if remote_file[-1] == '/' and encrypt: VimboxClientError('\nOnly files can be encrypted\n') return remote_file, force_creation, encrypt, initial_text
def sync(self, remote_file, remove_local=None, force_creation=False, register_folder=True, password=None, automerge_rules=None, amerge_ref_is_local=False, edits=None): """ Syncronize remote and local content with optional edit Edits will happen on a local copy that will be uploded when finished. remove_local After sucesful push remove local content force_creation Mandatory for new file on remote register_folder Store file path in local cache password Optional encryption/decription on client side automerge_rules Allowed way to automerge amerge_ref_is_local If valid automerge use local as reference (default is remote) """ # Sanity checks if remote_file[-1] == '/': raise VimboxClientError("Can not edit folders") if remove_local is None: remove_local = self.config['remove_local'] # Fetch remote content, merge if neccesary with local.mergetool # will provide local, remote and merged copies content, fetch_status, password = self.pull( remote_file, force_creation, password=password, automerge_rules=automerge_rules, amerge_ref_is_local=amerge_ref_is_local) # Apply edit if needed local_file = self.get_local_file(remote_file) dirname = os.path.dirname(local_file) if not os.path.isdir(dirname): os.mkdir(dirname) if edits: content['edited'] = edits(local_file, content) else: # No edits (still need to be sure we update local) content['edited'] = local.local_edit(local_file, content['merged'], no_edit=True) # Update local and remote # Abort if file being created but no changes if (content['edited'] is None and content['remote'] is not None and fetch_status != 'connection-error'): # For debug purposes VimboxClientError( "\nInvalid state: edited local_file non existing but remote" " does\n") # Pull again if recovered offline status if fetch_status == 'connection-error': content2, fetch_status, password = self.pull( remote_file, force_creation, password=password, automerge_rules=automerge_rules, amerge_ref_is_local=amerge_ref_is_local) if fetch_status != 'connection-error': content = content2 content['edited'] = content['remote'] self.update_rules(remote_file, content, password, fetch_status, register_folder, remove_local) return content['local'] == content['remote']
def copy(self, remote_source, remote_target): """ This should support: cp /path/to/file /path/to/another/file (file does not exist) cp /path/to/file /path/to/folder/ cp /path/to/folder/ /path/to/folder2/ cp /path/to/folder/ /path/to/folder2/ (folder2 does not exist) """ # Note file_type enforces using / for folders target_type, _, status = self.file_type(remote_target) if target_type == 'file': raise VimboxClientError('Target file %s exists' % remote_target) elif target_type == 'dir': if remote_source[-1] != '/': # cp /path/to/file /path/to/folder/ # map to cp /path/to/file /path/to/folder/file source_basename = os.path.basename(remote_source) remote_target = remote_target + '/' + source_basename else: # cp /path/to/folder/ /path/to/folder2/ # map to cp /path/to/folder/ /path/to/folder2/folder/ source_basename = os.path.basename(remote_source[:-1]) remote_target = remote_target + source_basename + "/" elif remote_source[-1] == '/': # cp /path/to/folder/ /path/to/folder2/ (folder2 does not exist) pass if status == 'connection-error': raise VimboxOfflineError("Connection error") # For folder we need to remove the ending back-slash if remote_source[-1] == '/': remote_source2 = remote_source[:-1] else: remote_source2 = remote_source if remote_target[-1] == '/': remote_target2 = remote_target[:-1] else: remote_target2 = remote_target response = self.client.files_copy(remote_source2, remote_target2) # If there is an error, try encrypted names is_encrypted = False if response['status'] == 'api-error': remote_source_hash = crypto.get_path_hash(remote_source2) remote_target_hash = crypto.get_path_hash(remote_target2) response = self.client.files_copy(remote_source_hash, remote_target_hash) is_encrypted = True if response['status'] == 'online': # Local move if we had a copy local_source = self.get_local_file(remote_source) local_target = self.get_local_file(remote_target) if os.path.isfile(local_source): # Make missing local folder local_target_folder = os.path.dirname(local_target) if not os.path.isdir(local_target_folder): os.makedirs(local_target_folder) shutil.move(local_source, local_target) elif os.path.isdir(local_source): shutil.copytree(local_source, local_target) # update cache and hash list if remote_source[-1] != '/': self.register_file(remote_target, is_encrypted) else: # If we are copying a folder we need to look for hashes inside # that folder and change their names self.register_file(remote_target, is_encrypted) self.copy_hash(remote_source, remote_target) if self.verbose > 0: items = (yellow("copied"), remote_source, remote_target) print("%-12s %s %s" % items) elif response['status'] == 'connection-error': raise VimboxOfflineError("Connection error") else: raise VimboxClientError("api-error: %s" % response['alerts'])
def list_folders(self, remote_folder): """ list folder content in remote """ # Try first remote if remote_folder and remote_folder[-1] == '/': response = self.client.list_folders(remote_folder[:-1]) else: response = self.client.list_folders(remote_folder) entries = response['content']['entries'] is_files = response['content']['is_files'] status = response['status'] message = response['alerts'] # Second try to see if there is an ecrypted file # TODO: entries is None is used to signal a this is a file not a # folder. This is an obscure way of dealing with this. is_encrypted = False if status == 'online' and entries is False: enc_remote_folder = crypto.get_path_hash(remote_folder) response = self.client.list_folders(enc_remote_folder) entries = response['content']['entries'] is_files = response['content']['is_files'] status = response['status'] message = response['alerts'] is_encrypted = status == 'online' display_string = "" if status == 'api-error': raise VimboxClientError("api-error") elif status == 'online' and entries is False: # Folder/File non existing raise VimboxClientError("%s does not exist in remote" % remote_folder) elif status == 'online' and entries is None: # This was a file return True elif status == 'online': # Differentiate file and folders display_folders = [] for entry, is_file in zip(entries, is_files): # Add slash to files on root if remote_folder == '': entry = '/' + entry if is_file: # File display_folders.append(entry) else: # Folder display_folders.append("%s/" % entry) display_folders = sorted(display_folders) # Update to match folder if remote_folder: # Remove folder paths no more in remote for path in self.config['cache']: if path[:len(remote_folder)] == remote_folder: cache_folder = \ path[len(remote_folder):].split('/')[0] + '/' if (cache_folder not in display_folders and cache_folder != ''): self.config['cache'].remove(path) # Add missing folders if remote_folder not in self.config['cache']: self.config['cache'].append(remote_folder) for folder in display_folders: if folder[-1] == '/': new_path = "%s%s" % (remote_folder, folder) if new_path not in self.config['cache']: self.config['cache'].append(new_path) # Write cache self.config['cache'] = sorted(self.config['cache']) local.write_config(self.config_path, self.config) # Replace encrypted files entry_types = [] new_display_folders = [] for entry in display_folders: key = "%s%s" % (remote_folder, entry) if key in self.config['path_hashes'].keys(): entry_types.append('encrypted') new_display_folders.append( os.path.basename(self.config['path_hashes'][key])) elif entry[-1] == '/': entry_types.append('folder') new_display_folders.append(entry) else: entry_types.append(None) new_display_folders.append(entry) display_folders = new_display_folders # Display entries sorted and with colors new_display_folders = [] indices = sorted(range(len(display_folders)), key=display_folders.__getitem__) for file_folder, entry_type in zip(display_folders, entry_types): if entry_type == 'encrypted': file_folder = red(file_folder) elif entry_type == 'folder': file_folder = blue(file_folder) new_display_folders.append(file_folder) display_string = "".join( ["%s\n" % new_display_folders[index] for index in indices]) # Add file to cache if remote_folder not in self.config['cache']: self.config['cache'].append(remote_folder) local.write_config(self.config_path, self.config) elif os.path.isdir(local.get_local_file(remote_folder)): # If it fails resort to local cache display_folders = local.list_local(remote_folder, self.config) if self.verbose > 0: print("\n%s content for %s " % (red("offline"), remote_folder)) display_string = "".join( ["%s\n" % folder for folder in sorted(display_folders)]) # Print if self.verbose > 0: print("\n%s\n" % display_string.rstrip())