Skip to content

Commit

Permalink
refactor!: class for each file type and extract assertion
Browse files Browse the repository at this point in the history
Closes #3
  • Loading branch information
Seb-sti1 committed Nov 12, 2024
1 parent 25024f0 commit 924ce0a
Show file tree
Hide file tree
Showing 10 changed files with 714 additions and 413 deletions.
71 changes: 60 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,45 @@ A command line tool to convert the files in `/home/root/.local/share/remarkable/
the [reMarkable 2](https://remarkable.com/) to PDFs.

**It is only compatible with `.rm` version 6
and `.content` version 1 and 2 _(`"formatVersion": 2` or `"formatVersion": 1`)_.**
and `.content` version 1 and 2.**
See [this section](#how-to-check-compatibility-and-update-my-files-to-v6) to verify compatibility.

It uses [rmc](https://github.com/ricklupton/rmc) (which depends on [rmscene](https://github.com/ricklupton/rmscene)) to
render the remarkable files to svg, converts them to pdf and then merges them together with, if necessary,
the background pdf. _Currently, [Seb-sti1/rmc](https://github.com/Seb-sti1/rmc/tree/dev) is used because it
uses [rmscene](https://github.com/ricklupton/rmscene) v0.5.0 instead of v0.2.0_
uses [rmscene](https://github.com/ricklupton/rmscene) v0.5.0 instead of v0.2.0._

_Please before submitting an issue check the [Known issues](#known-issues) section and the already existing issues._
_Please before submitting an issue check the [known issues](#major-known-issues) section and the already existing
issues._

## How to install?

Start installing the dependencies:
Start by installing the dependency:

```sh
apt install libcairo2
python3.11 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

_As an alternative, you can also build the rmtree package and install it using pipx_
Then download the `.whl` from the last release.

### Using pipx (recommended)

See the [official pipx documentation](https://pipx.pypa.io/stable/installation/) on how
to install pipx.

```sh
pipx install ./rmtree.whl
```

### Using venv

```sh
python3.11 -m venv .venv # create a venv
source .venv/bin/activate # source the venv
pip install ./rmtree.whl
```

_Note: in the following instead of `rmtree`, you should use `python -m rmtree` to start the package._

## How to use?

Expand All @@ -34,14 +52,45 @@ After installing, it is intended to be used with the following commands:
```sh
# replace [ip] by the ip of the remarkable.
rsync -r --delete --progress root@[ip]:/home/root/.local/share/remarkable/xochitl/ rm_folder/
python -m rmtree ./rm_folder ./exported_file_destination
rmtree ./rm_folder ./exported_file_destination
```

_If you have assertion errors, you can ignore them using the `--ignore-assertion` option but
there is a high chance that the output will not be correct.
For more information or compatibility errors see
[the next section](#how-to-check-compatibility-and-update-my-files-to-v6)._

## How to check compatibility and update my files to v6?

[_Hopefully soon_](https://github.com/Seb-sti1/rmtree/issues/3)
You can test the program for compatibility and assertion errors using
`rmtree ./rm_folder --test-compatibility`. It will output the detected compatibility
and assertion errors.

- Compatibility refers to the constraint that I decided to impose (mainly the
version constraint on .rm and .content files). Any error regarding this is
considered 'wrong input from the user'/'software limitation' and not a bug.
- Assertion refers to the hypothesis made as there is no official API for
the reMarkable file structure. Errors regarding these assertions can be
considered bugs. Please report them on GitHub with as much information as possible.

### Update rm files to v6

Compatibility error regarding the `.rm` files version will be listed as shown below:

```
The following are compatibility errors. This software is explicitly not compatible with those files.
You can look at the README.md to find more information:
https://github.com/Seb-sti1/rmtree?tab=readme-ov-file#how-to-check-compatibility-and-update-my-files-to-v6.
- Notes (page n°1, 2) (25a92754-ea08-467a-a386-5e169c804c96): This software is only compatible with rm file version 6
- Notebook (page n°1, 2, 3, 4, 5) (fb90bd3d-62d2-46f6-a7e3-aa098c1938f2): This software is only compatible with rm file version 6
```

This indicates that `Notes` page number 1 and 2 and `Notebook` page number 1, 2, 3, 4 and 5 are not using
`.rm` file v6.
**It appears that going to each page individually and drawing (even if removing afterward) makes the
reMarkable updates the page to v6.**

## Known issues
## Major known issues

- [Background template aren't exactly aligned](https://github.com/Seb-sti1/rmc/issues/4)
- [Missing background templates](https://github.com/Seb-sti1/rmtree/issues/4)
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
packages = ["rmtree"]
packages = ["rmtree", "rmtree.struct"]

[project]
name = "rmtree"
version = "0.2.1a"
version = "1.0.0"
requires-python = ">=3.10"
readme = "README.md"
license = { file = "LICENSE" }
Expand Down
170 changes: 156 additions & 14 deletions rmtree/debug.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,167 @@
import os
from typing import Dict
import typing as tp
from pathlib import Path

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

def count_extension(src) -> Dict[str, int]:
"""
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.
- .tree: TBD
- [uuid].local: TBD
- [uuid].content: Contains information on the actual content (like pages, page count, etc)
- [uuid].metadata: Contains the metadata (like the name, parent file, etc)
- [uuid].pagedata: TBD
- [uuid].pdf: The background PDF (if any)
- [uuid]/: The folder containing the pages
- [page uuid].rm: reMarkable binary files
- [page uuid]-metadata.json: TBD
- [uuid].thumbnails/: The folder containing PNG of the pages (probably used for preview)
- [uuid].highlights/: TBD
- [uuid].textconversion/: TBD
"""

know_file_extensions = ["tombstone", "local", "metadata", "content", "pagedata", "pdf", "dirty"]
know_folder_extensions = ["thumbnails", "highlights", "textconversion", "RM_FOLDER"]


def count_extension(src: Path) -> tp.Tuple[tp.Dict[str, int], tp.Dict[str, int]]:
"""
Count the number of files of each extension in the `src` folder
:param src: the source folder
:return: A dictionary with number of files of each extension
:return: A dictionary with number of files of each extension, A dictionary with number of folder of each extension
"""

extension = {}

for filename in os.listdir(src):
ext = os.path.splitext(filename)
ext = ext[-1] if ext[-1].startswith(".") else ext[-2]
file_extension = {}
folder_extension = {}

if not ext.startswith("."):
ext = "folder"
for filename in [f for f in os.listdir(src) if ID_PATTERN.fullmatch(f) is not None]:
if os.path.isfile(os.path.join(src, filename)):
ext = filename.split(".")[-1]

if ext in extension:
extension[ext] += 1
if ext in file_extension:
file_extension[ext] += 1
else:
file_extension[ext] = 1
else:
extension[ext] = 1
return extension
ext = filename.split(".")[-1] if "." in filename else "RM_FOLDER"

if ext in folder_extension:
folder_extension[ext] += 1
else:
folder_extension[ext] = 1

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"
"\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()

extensions = count_extension(src)
custom_print("Extension of detected files:")
custom_print("\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} "
f"{'' if ext in know_folder_extensions else '(unknown, please consider submitting an issue)'}: {c}"
for ext, c in extensions[1].items()]))

uuid_list = list(set([ID_PATTERN.fullmatch(f).group(1) for f in os.listdir(src)
if ID_PATTERN.fullmatch(f) is not None]))

def exists(name: str):
return os.path.exists(os.path.join(src, name))

errors = {}
for uuid in uuid_list:
# if it's a tombstone or dirty file then there are no other available files
if (exists(uuid + ".tombstone") or exists(uuid + ".dirty")) and \
any([exists(uuid + "." + ext) for ext in know_file_extensions + know_folder_extensions
if ext not in ["tombstone", "dirty", "RM_FOLDER"]]
+ [exists(uuid)]):
errors[uuid] = {"type": "assert",
"reason": "tombstone or dirty is present with useful files."}
continue

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

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

# verify assertion on the content file
content = metadata.get_associated_content()
if isinstance(content, ContentFile) and content.get_version() not in [1, 2]:
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."}
continue

# check rm file version
if isinstance(content, ContentFile):
not_compatible_pages: list[tp.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()))

if len(not_compatible_pages) > 0:
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"}

# 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(
"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(
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']}")

# 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(
"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']}")

return len(compatibility_errors), len(assertion_errors)
Loading

0 comments on commit 924ce0a

Please sign in to comment.