Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ packages = ["rmtree", "rmtree.struct"]

[project]
name = "rmtree"
version = "1.2.0"
version = "2.0.1"
requires-python = ">=3.10"
readme = "README.md"
license = { file = "LICENSE" }
Expand All @@ -25,4 +25,4 @@ dependencies = [


[project.scripts]
rmtree = "rmtree.main:main"
rmtree = "rmtree.__main__:main"
46 changes: 45 additions & 1 deletion rmtree/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,48 @@
from .main import main
import argparse
import logging
from pathlib import Path

from tqdm import tqdm

from rmtree.debug import test_assertion
from rmtree.logger import setup_logging
from rmtree.struct.file import list_files

logger = logging.getLogger(__name__)


def main(args=None):
parser = argparse.ArgumentParser("rmtree", description="Process the file tree of the reMarkable tablet.")
parser.add_argument("src", type=Path, help="The source folder.")
parser.add_argument("dst", type=Path, nargs="?", help="The folder where the files are exported to.")
parser.add_argument("--debug", "-d", action="store_true", help="Set all loggers to DEBUG (including rmscene).")
parser.add_argument("--test-compatibility", "-t", action="store_true",
help="Test if the files from the reMarkable are compatible with this program.")
parser.add_argument("--ignore-assertion", "-ia", action="store_true",
help="Continue despite assertion errors. Output correctness is not guaranteed.")
parser.add_argument("--ignore-compatibility", "-ic", action="store_true",
help="Continue despite compatibility errors. WARNING: the output will not be correct.")
args = parser.parse_args(args)

if not (args.dst or args.test_compatibility):
parser.error("Please specify the 'dst' argument.")

setup_logging(args.debug, args.test_compatibility)
is_compatible, are_assertions_correct = test_assertion(args.src)

if ((not args.test_compatibility)
and (are_assertions_correct or args.ignore_assertion)
and is_compatible or args.ignore_compatibility):
# export all the files
files = list_files(args.src)
progress = tqdm(files.items())
for uuid, f in progress:
progress.set_description(str(f))
try:
f.export(args.dst.joinpath(f.get_path(files)))
except Exception as e:
logger.error(f"Failed to export {f.get_name()}: {e}")


if __name__ == "__main__":
main()
91 changes: 50 additions & 41 deletions rmtree/debug.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import logging
import os
import typing as tp
from pathlib import Path
from typing import Tuple, Dict

from rmtree.struct.content import ContentFile
from rmtree.struct.content import FileType, ContentFile
from rmtree.struct.file import ID_PATTERN
from rmtree.struct.metadata import Metadata
from rmtree.struct.page import PageRM, PageVersion

logger = logging.getLogger(__name__)

"""
Here is a list and partial description of the files in the /home/root/.local/share/remarkable/xochitl/ of the
reMarkable. `[uuid]` represents an uuid v4.
Expand All @@ -29,7 +32,7 @@
know_folder_extensions = ["thumbnails", "highlights", "textconversion", "RM_FOLDER"]


def count_extension(src: Path) -> tp.Tuple[tp.Dict[str, int], tp.Dict[str, int]]:
def count_extension(src: Path) -> Tuple[Dict[str, int], Dict[str, int]]:
"""
Count the number of files of each extension in the `src` folder
:param src: the source folder
Expand Down Expand Up @@ -58,25 +61,23 @@ def count_extension(src: Path) -> tp.Tuple[tp.Dict[str, int], tp.Dict[str, int]]
return file_extension, folder_extension


def test_assertion(src: Path, custom_print=print) -> tp.Tuple[int, int]:
custom_print(f"===== Testing compatibility and assertion on {src} =====")
custom_print()
custom_print("Be aware of the following:")
custom_print("\t- Compatibility refers to the constraint that I decided to impose (mainly the\n"
def test_assertion(src: Path) -> Tuple[bool, bool]:
logger.info(f"===== Testing compatibility and assertion on {src} =====\n")
logger.info("Be aware of the following:")
logger.info("\t- Compatibility refers to the constraint that I decided to impose (mainly the\n"
"\t version constraint on .rm and .content files). Any error regarding this is\n"
"\t considered 'wrong input from the user'/'software limitation' and not a bug.\n"
"\t- Assertion refers to the hypothesis made as there is no official API for\n"
"\t the reMarkable file structure. Errors regarding these assertions can be\n"
"\t considered bugs. Please report them on GitHub with as much information as possible.")
custom_print()
"\t considered bugs. Please report them on GitHub with as much information as possible.\n")

extensions = count_extension(src)
custom_print("Extension of detected files:")
custom_print("\n".join([f"\t- {ext} "
logger.info("Extension of detected files:")
logger.info("\n".join([f"\t- {ext} "
f"{'' if ext in know_file_extensions else '(unknown, please consider submitting an issue)'}: {c}"
for ext, c in extensions[0].items()]))
custom_print("Extension of detected folder:")
custom_print("\n".join([f"\t- {ext} "
logger.info("Extension of detected folder:")
logger.info("\n".join([f"\t- {ext} "
f"{'' if ext in know_folder_extensions else '(unknown, please consider submitting an issue)'}: {c}"
for ext, c in extensions[1].items()]))

Expand All @@ -98,32 +99,38 @@ def exists(name: str):
continue

# otherwise there should be a metadata and a content
if not (exists(uuid + ".metadata") and exists(uuid + ".content")):
if not exists(uuid + ".metadata"):
errors[uuid] = {"type": "assert",
"reason": "Either the metadata or content file is missing."}
"reason": "The metadata is missing."}
continue

# verify assertion on the metadata file
metadata = Metadata(src, uuid)
if not metadata.test_assertion(uuid_list):
r = metadata.test_assertion(uuid_list)
if r is not True:
errors[uuid] = {"type": "assert",
"reason": "The metadata assertion are not verified."}
"reason": r}
continue

# verify assertion on the content file
content = metadata.get_associated_content()
if isinstance(content, ContentFile) and content.get_version() not in [1, 2]:
# .content is optional for folders
if metadata.get_file_type() == FileType.DOCUMENT and content is None:
errors[uuid] = {"name": metadata.get_name(), "type": "compatibility",
"reason": "This software is only compatible with content version 1 and 2."}
continue
elif not content.test_assertion():
errors[uuid] = {"name": metadata.get_name(), "type": "assert",
"reason": "The content assertion are not verified."}
"reason": "The .content is missing."}
continue

# check rm file version
# verify assertion on the .content
if content is not None:
r = content.test_assertion()
if r is not True:
errors[uuid] = {"name": metadata.get_name(), "type": "compatibility",
"reason": r}
continue

# check assertion specific to documents
if isinstance(content, ContentFile):
not_compatible_pages: list[tp.Tuple[int, str, PageVersion]] = []
# check rm file version
not_compatible_pages: list[Tuple[int, str, PageVersion]] = []
for n, page in enumerate(content.get_pages()):
if isinstance(page, PageRM) and not page.test_assertion():
not_compatible_pages.append((n + 1, page.page_uuid, page.get_version()))
Expand All @@ -132,36 +139,38 @@ def exists(name: str):
errors[uuid] = {"name": metadata.get_name(), "pages": not_compatible_pages, "type": "compatibility",
"reason": "This software is only compatible with rm file version 6"}

# [uuid]/ folder only contains rm files
if exists(uuid):
for filename in os.listdir(os.path.join(src, uuid)):
if not (filename.endswith(".rm") or filename.endswith("-metadata.json")):
errors[uuid] = {"name": metadata.get_name(), "type": "assert",
"reason": "Unknown files in the [uuid]/ folder"}
# [uuid]/ folder only contains rm files
if exists(uuid):
for filename in os.listdir(os.path.join(src, uuid)):
if not (filename.endswith(".rm") or filename.endswith("-metadata.json")):
errors[uuid] = {"name": metadata.get_name(), "type": "assert",
"reason": "Unknown files in the [uuid]/ folder"}

# print compatibilities errors
compatibility_errors = {uuid: error for uuid, error in errors.items() if error["type"] == "compatibility"}
if len(compatibility_errors) > 0:
custom_print()
custom_print(
logger.info(
"The following are compatibility errors. This software is explicitly not compatible with those files.\n"
"You can look at the README.md to find more information:\n"
"https://github.com/Seb-sti1/rmtree?tab=readme-ov-file#how-to-check-compatibility-and-update-my-files-to-v6.")
for uuid, error in compatibility_errors.items():
if "pages" in error:
custom_print(
logger.info(
f"\t- {error['name']} (page n°{', '.join([str(pages[0]) for pages in error['pages']])}) ({uuid}):"
f" This software is only compatible with rm file version 6")
else:
custom_print(f"\t- {error['name']} ({uuid}): {error['reason']}")
logger.info(f"\t- {error['name']} ({uuid}): {error['reason']}")

# print assertion errors
assertion_errors = {uuid: error for uuid, error in errors.items() if error["type"] == "assert"}
if len(assertion_errors) > 0:
custom_print()
custom_print(
logger.info(
"The following are assertion errors. You can report them at https://github.com/Seb-sti1/rmtree/issues.")
for uuid, error in assertion_errors.items():
custom_print(f"\t- {error['name'] if 'name' in error else 'Unknown name'} ({uuid}): {error['reason']}")
logger.info(f"\t- {error['name'] if 'name' in error else 'Unknown name'} ({uuid}): {error['reason']}")

return len(compatibility_errors), len(assertion_errors)
if len(compatibility_errors) > 0:
logger.warning(f"{len(compatibility_errors)} detected compatibility errors.")
if len(assertion_errors) > 0:
logger.warning(f"{len(assertion_errors)} detected assertion errors.")
return len(compatibility_errors) == 0, len(assertion_errors) == 0
31 changes: 31 additions & 0 deletions rmtree/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import logging
import sys


class ColorFormatter(logging.Formatter):
COLORS = {
logging.DEBUG: "\033[36m", # Cyan
logging.INFO: "\033[0m", # Default terminal color
logging.WARNING: "\033[33m", # Dark yellow / amber
logging.ERROR: "\033[31m", # Red
logging.CRITICAL: "\033[1;31m", # Bold red
}
RESET = "\033[0m"

def format(self, record):
prefix = ""
if logging.getLogger("rmtree").level == logging.DEBUG:
prefix = f"[{record.name}] "
color = self.COLORS.get(record.levelno, self.RESET)
return f"{prefix}{color}{record.getMessage()}{self.RESET}"


def setup_logging(debug: bool, test_compatibility: bool) -> None:
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(ColorFormatter())
logging.basicConfig(handlers=[handler], level=logging.DEBUG if debug else logging.INFO)
logging.getLogger("rmtree").setLevel(logging.DEBUG if debug else logging.INFO)
logging.getLogger("rmtree.debug").setLevel(logging.DEBUG if debug else
logging.INFO if test_compatibility else
logging.WARNING)
logging.getLogger("rmscene").setLevel(logging.DEBUG if debug else logging.CRITICAL)
71 changes: 0 additions & 71 deletions rmtree/main.py

This file was deleted.

Loading
Loading