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 _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 register_file(remote_file, config, is_encripted): """ Add folder of file to cache and file to hash list if encrypted A file can be registered by its folder or it name directly """ assert remote_file, "file to register cannot be empty path" rewrite_config = False # Register folder in cache if remote_file[-1] == '/': remote_folder = remote_file else: remote_folder = "%s/" % os.path.dirname(remote_file) # if not is_registered and list(set(remote_folder))[0] != '/': if remote_folder not in config['cache']: config['cache'].append(remote_folder) rewrite_config = True # update cache in system print("Added to cache %s" % remote_folder) # Register file hash in the local cache if is_encripted: remote_file_hash = crypto.get_path_hash(remote_file) if remote_file_hash not in config['path_hashes']: config['path_hashes'][remote_file_hash] = remote_file rewrite_config = True print("Added to hash list %s" % remote_file) if rewrite_config: write_config(CONFIG_FILE, config)
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 _push(self, new_local_content, remote_file, password=None): """ Push updates to remote NOTE: This overwrites remote content. It can lead to loss of data. """ # If encrypted get encrypted remote-name if password is not None: # Validate pasword validated_password = crypto.validate_password(password) # Hash filename remote_file_hash = crypto.get_path_hash(remote_file) # Encript content new_local_content = crypto.encrypt_content(new_local_content, validated_password) else: remote_file_hash = remote_file if sys.version_info[0] > 2: # Encoding for Python3 new_local_content = str.encode(new_local_content) # Overwrite remote self.client.files_upload(new_local_content, remote_file_hash)
def read_remote_content(remote_file, password=None): if password: password = crypto.validate_password(password) remote_file = crypto.get_path_hash(remote_file) true_path = get_fake_remote_local_path(remote_file) with open(true_path, 'rb') as fid: text = fid.read() text, _ = crypto.decript_content(text, password) else: true_path = get_fake_remote_local_path(remote_file) with open(true_path, 'rb') as fid: text = fid.read() # Python3 if text and sys.version_info[0] > 2 and not isinstance(text, str): try: text = text.decode("utf-8") except UnicodeDecodeError: # Encrypted content pass return text
def hash_is_registered(encrypted_file): key = (get_path_hash(encrypted_file), encrypted_file) return key in load_config()['path_hashes'].items()
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())
def install_backend(config_file, default_config): if os.path.isfile(config_file): config = local.read_config(config_file) if 'DROPBOX_TOKEN' in config: print("Found valid config in %s" % config_file) # check for updated hashes from < v0.5.0 if int(__version__.split('.')[1]) >= 5: from vimbox.crypto import get_path_hash client = StorageBackEnd(config['DROPBOX_TOKEN']) old_hashes = config['path_hashes'].items() new_hashes = [] for dhash, path in old_hashes: new_dhash = get_path_hash(path, md5_hash=False) if dhash != new_dhash: # Change remote path client.files_copy(dhash, new_dhash) client.files_delete(dhash) new_hashes.append((str(new_dhash), str(path))) config['path_hashes'] = dict(new_hashes) local.write_config(config_file, config) print("Updated %d hashes" % len(new_hashes)) else: # Prompt user for a token print( "\nI need you to create a dropbox app and give me an acess token." " Go here \n\nhttps://www.dropbox.com/developers/apps/\n\n" "1) Create App, select Dropbox API\n" "2) Select either App folder or Full Dropbox\n" "3) Name is irrelevant but vimbox may help you remember\n" ) if sys.version_info[0] > 2: dropbox_token = input( "Press \"generate acess token\" to get one and paste it here: " ) else: dropbox_token = raw_input( "Press \"generate acess token\" to get one and paste it here: " ) # Validate token by connecting to dropbox client = StorageBackEnd(dropbox_token) response = client.get_user_account() if response['content'] is None: print("Could not connect to dropbox %s" % response['status']) exit(1) else: user_acount = response['content'] print("Connected to dropbox account %s (%s)" % ( user_acount.name.display_name, user_acount.email) ) # Store config = default_config config['DROPBOX_TOKEN'] = dropbox_token local.write_config(config_file, config) print("Created config in %s" % config_file)
def is_fake_remote_file(file_path, password=None): if password: file_path = crypto.get_path_hash(file_path) remote_file = get_fake_remote_local_path(file_path) return os.path.isfile(remote_file)