def test_recover_or_create(Path, tmpdir): """Test the function that works out the file and path for the config. """ Path.home.side_effect = lambda: tmpdir cfg = userconfig.recover_or_create() base_dir = tmpdir / '.rmfriend' assert base_dir.isdir() notebooks = base_dir / 'notebooks' assert notebooks.isdir() config_file = base_dir / 'config.cfg' assert config_file.isfile() assert 'rmfriend' in cfg assert cfg['rmfriend']['address'] == '10.11.99.1' assert cfg['rmfriend']['port'] == '22' assert cfg['rmfriend']['username'] == 'root' assert cfg['rmfriend']['cache_dir'] == str(notebooks) assert cfg['rmfriend']['remote_dir'] == ( '/home/root/.local/share/remarkable/xochitl') # Calling the second time will re-read from the already present config # file so there should be no problems. cfg = userconfig.recover_or_create() assert 'rmfriend' in cfg assert cfg['rmfriend']['address'] == '10.11.99.1' assert cfg['rmfriend']['port'] == '22' assert cfg['rmfriend']['username'] == 'root' assert cfg['rmfriend']['cache_dir'] == str(notebooks) assert cfg['rmfriend']['remote_dir'] == ( '/home/root/.local/share/remarkable/xochitl')
def save_configuration(): """Save the configuration to disk.""" config = userconfig.recover_or_create() settings = request.json() print('Saving settings: {}'.format(settings)) config['rmfriend']['address'] = settings['address'] config['rmfriend']['post'] = settings['post'] config['rmfriend']['username'] = settings['username'] config['rmfriend']['cache_dir'] = settings['cache_dir'] config.write() config = userconfig.recover_or_create() return json.dumps(dict(config['rmfriend']))
def notebook_cache(cls): """ """ notebooks = collections.defaultdict(dict) config = userconfig.recover_or_create() cache_dir = Path(config['rmfriend']['cache_dir']) for item in cache_dir.iterdir(): if item.is_dir(): # Skip for the moment continue doc_id, ext = document_id_and_extension(item.name) uri = item.as_uri() version = '-' name = '-' if ext == 'metadata': metadata = json.loads(item.read_bytes()) name = metadata['visibleName'] version = metadata['version'] notebooks[doc_id][ext] = { 'uri': uri, 'version': version, 'name': name, } return dict(notebooks)
def recover(cls, sftp, document_id): """Recover a remote notebook from reMarkable. :param sftp: A connected paramiko SFTP instance. This should have changed directory to the one containing the notebooks. :param document_id: The UUID string for the notebook. This will attempt to recover the files <document_id>.(lines|metadata|content|pagedata) The notebook directories (thumbnails and cache) will also be recovered. :returns: A Notebook instance. """ config = userconfig.recover_or_create() cache_dir = Path(config['rmfriend']['cache_dir']) address = config['rmfriend']['address'] username = config['rmfriend']['username'] auth = dict( hostname=address, username=username, ) for extension in ('lines', 'metadata', 'content', 'pagedata'): # Recover to the cache: local_file = filename_from(document_id, extension, cache_dir) remote_file = filename_from(document_id, extension) try: sftp.get(remote_file, localpath=local_file) except IOError as error: print('Error recovering {} to {}: {}'.format( remote_file, local_file, error)) def get_(remote_dir, remote_file, local_file): with SFTP.connect(**auth) as sftp: sftp.chdir(remote_dir) sftp.get( remote_file, localpath=local_file, ) for extension in ('thumbnails', 'cache'): with SFTP.connect(**auth) as sftp: name = filename_from(document_id, extension) local_dir = Path( filename_from(document_id, extension, cache_dir)) dirname = cache_dir / name if not dirname.is_dir(): os.makedirs(dirname) # Iterate through each image and recover it: sftp.chdir(name) for item in sftp.listdir_iter(): local_file = str(local_dir / item.filename) remote_file = item.filename get_(name, remote_file, local_file)
def static(document_id, filepath): """Recover the current notebooks from cache. """ config = userconfig.recover_or_create() cache_dir = config['rmfriend']['cache_dir'] filename = "{}/{}.thumbnails/{}".format(cache_dir, document_id, filepath) with open(filename, 'rb') as fd: returned = fd.read() return returned
def do_notebook_ls(self, subcmd, opts, *args, **kwargs): """${cmd_name}: Show a list of notebooks on reMarkable. ${cmd_usage} ${cmd_option_list} """ config = userconfig.recover_or_create() address = config['rmfriend']['address'] username = config['rmfriend']['username'] if opts.ask: password = getpass.getpass( "Please enter password for {}@{}: ".format(username, address) ) else: password = opts.password auth = dict( hostname=config['rmfriend']['address'], username=config['rmfriend']['username'], password=password, ) with SFTP.connect(**auth) as sftp: results = SFTP.notebooks_from_listing(sftp.listdir()) listing = SFTP.notebook_ls(sftp, results) table_listing = [ ['Last Modified', 'Name', 'reMarkable Version', 'Local Version'] ] if opts.show_id: table_listing[0].insert(0, 'ID') for e in listing: if opts.show_id: table_listing.append(( e['id'], e['last_modified'], e['name'], e['version'], e['local_version'] ) ) else: table_listing.append(( e['last_modified'], e['name'], e['version'], e['local_version'] )) table = AsciiTable(table_listing) print(table.table)
def notebook_previews(cls): """ """ config = userconfig.recover_or_create() cache_dir = Path(config['rmfriend']['cache_dir']) found = [] for item in cache_dir.iterdir(): document_id, ext = document_id_and_extension(item.name) listing = { "id": document_id, "name": '', "version": '', "last_modified": '', "last_opened": '', "images": [], } if ext == 'thumbnails': # recover metadata details: name = filename_from(document_id, 'metadata') metadata = cache_dir / name metadata = json.loads(metadata.read_bytes()) listing['name'] = metadata['visibleName'] listing['version'] = metadata['version'] listing['last_modified'] = metadata['lastModified'] # Is the last opened page present? name = filename_from(document_id, 'content') content = cache_dir / name if content.is_file(): content = json.loads(content.read_bytes()) if 'lastOpenedPage' in content: listing['last_opened'] = content['lastOpenedPage'] # find the thumbnail images: name = filename_from(document_id, 'thumbnails') thumbnails = cache_dir / name for item in thumbnails.iterdir(): listing['images'].append(item.name) listing['images'] = natsorted( listing['images'], alg=ns.IGNORECASE ) found.append(listing) # Sort by last modified decending emulating reMarkable / Notebooks UI found = sorted( found, key=lambda doc: doc['last_modified'], reverse=True ) return found
def do_recover(self, subcmd, opts, document_id): """${cmd_name}: Recover a specific notebook to the local cache. The given document will be recovered regardless of whether the same files already exist. ${cmd_usage} ${cmd_option_list} """ config = userconfig.recover_or_create() auth = dict( hostname=config['rmfriend']['address'], username=config['rmfriend']['username'], ) with SFTP.connect(**auth) as sftp: Notebook.recover(sftp, document_id)
def rsync(cls): """ """ config = userconfig.recover_or_create() address = config['rmfriend']['address'] username = config['rmfriend']['username'] local_notebooks = Sync.notebook_cache() local = set(local_notebooks.keys()) def progress_factory(message): def action_ticker(total, position): done = int((position / total) * 100) sys.stdout.write( '\r{}: {:2d}% '.format(message, done) ) sys.stdout.flush() return action_ticker auth = dict( hostname=address, username=username, ) with SFTP.connect(**auth) as sftp: notebook_listing = SFTP.notebooks_from_listing(sftp.listdir()) remote_notebooks = { nb['id']: nb for nb in SFTP.notebook_ls(sftp, notebook_listing) } remote = set(remote_notebooks.keys()) print("All notebooks on reMarkable: {}".format(len(remote))) only_local = local.difference(remote) print("Notebooks only present locally: {}".format(len(only_local))) present_on_both = local.union(remote) print("Notebooks on both: {}".format(len(present_on_both))) only_remote = remote.difference(local) print("Notebooks only on reMarkable: {}".format(len(only_remote))) change_progress = utils.progress_factory('Working out changes') changed_notebooks = [] with SFTP.connect(**auth) as sftp: progress = 1 total = len(list(present_on_both)) for doc_id in present_on_both: change_progress(progress, total) progress += 1 if doc_id not in remote_notebooks: # only local, ignore. continue local_version = remote_notebooks[doc_id]['local_version'] remarkable_version = remote_notebooks[doc_id]['version'] if remarkable_version > local_version: print( "Recovering doc_id: ", doc_id, " local_version: ", local_version, " remarkable_version: ", remarkable_version ) Notebook.recover(sftp, doc_id) recover_progress = utils.progress_factory('Recovering new notebooks') auth['ssh_only'] = False with SFTP.connect(**auth) as sftp: # clear change progress update. progress = 1 total = len(only_remote) for document_id in only_remote: Notebook.recover(sftp, document_id) recover_progress(progress, total) progress += 1 returned = { 'new': list(only_remote), 'deleted': list(only_local), 'changed': list(changed_notebooks), } print("\nDone") return returned
def notebook_ls(cls, sftp, notebooks): """Recover the metadata and print a listing of the notebooks. :param sftp: See connect() for details. :param notebooks: See notebooks_from_listing(). :returns: A list of notebook information or an empty list. E.g.:: [ { 'id': 'UUID', 'name': 'notebook name', 'version': '<reMarkable notebook version number>', 'local_version': '<cached notebook version number>' or '', 'last_modified': 'iso8601 formatted string' }, : etc ] This only contains the information and not the actual notebook lines data. """ config = userconfig.recover_or_create() cache_dir = Path(config['rmfriend']['cache_dir']) listing = [] for document_id in notebooks: file_ = "{}.{}".format(document_id, 'metadata') try: data = cls.get(sftp, file_) except IOError as error: # noqa # print('Error reading {}: {}'.format(file_, error)) pass else: notebooks[document_id]['metadata'] = json.loads(data) # If there is a local version of this file check its version local_version = 0 local_metadata = cache_dir / file_ if local_metadata.is_file(): local_metadata = json.loads(local_metadata.read_bytes()) local_version = int(local_metadata['version']) metadata = notebooks[document_id]['metadata'] last_modified = int(metadata['lastModified']) / 1000 last_modified = time.strftime( '%Y-%m-%d %H:%M:%S', time.gmtime(last_modified) ) if metadata['type'] == 'DocumentType': listing.append({ 'id': document_id, 'name': metadata['visibleName'], 'version': int(metadata['version']), 'local_version': local_version, 'last_modified': last_modified, }) else: # This could be a collection type, PDF, epub, etc. pass # print('Not a notebook: {} {}'.format( # metadata['type'], metadata['visibleName'] # )) return listing
def recover_configuration(): """Recover current configuration file contents.""" config = userconfig.recover_or_create() return json.dumps(dict(config['rmfriend']))