Skip to content

Commit 496f57d

Browse files
slowly refactoring and exploring alternative designs, expecting tests to break for now
1 parent f294b02 commit 496f57d

5 files changed

Lines changed: 123 additions & 53 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ This tool will *probably* work on any of the Supernote devices running the most
4646
snbackup -c /the/path/to/config.json
4747
```
4848

49-
- You can also set the environment variable `SNBACKUP_CONF` which points to the location of the **_config.json_**. This allows you to run `snbackup` from anywhere without needing to specify the config file location. The exact command to set environment variables will depend on your operating system and terminal shell.
49+
- You can also set the environment variable `SNBACKUP_CONF` which points to the location of the **_config.json_**. This allows you to run `snbackup` from anywhere without needing to specify the config file location. The exact command to set environment variables will depend on your operating system and terminal shell. Setting the environment variable will take priority over the other config lookup methods.
5050
```bash
5151
export SNBACKUP_CONF="/path/to/config.json"
5252
```

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ exclude = [
6969
"venv",
7070
]
7171

72-
line-length = 140
72+
line-length = 120
7373
indent-width = 4
7474

7575
target-version = "py310"

snbackup/backup.py

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77

88
import httpx
99

10-
from .files import SnFile
10+
from .files import SnFile, FileMeta
11+
from .setup import SetupConf
1112
from .utilities import CustomLogger, truncate_log
1213
from .helpers import (
13-
EXTS, FOLDERS, user_input, check_version, today_pth, load_config,
14-
bytes_to_mb, count_backups, recursive_scan, locate_config
14+
EXTS, FOLDERS, user_input, check_version,
15+
today_pth, load_config, bytes_to_mb,
16+
count_backups, recursive_scan, locate_config
1517
)
1618

1719

@@ -77,6 +79,18 @@ def device_uri_gen(url: str, note_details: list[dict]):
7779
yield from device_uri_gen(url, device_data)
7880

7981

82+
def extract_meta(url: str, file_details: list[dict]):
83+
"""Recursive generator to extract data to instantiate SnFile object"""
84+
for data in file_details:
85+
if not data.get('isDirectory'):
86+
# Drop the anchor slash to call joinpath later and it work properly
87+
yield data.get('uri').lstrip('/'), data.get('date'), data.get('size')
88+
else:
89+
html = talk_to_device(url, data.get('uri').lstrip('/'))
90+
re_parse = parse_html(html.text)
91+
device_data = load_parsed(re_parse)
92+
yield from device_uri_gen(url, device_data)
93+
8094
# def save_file(local_pth: Path, file: bytes) -> None:
8195
# """Make parent directory and write file bytes object to local disk"""
8296
# local_pth.parent.mkdir(exist_ok=True, parents=True)
@@ -108,16 +122,23 @@ def previous_record_gen(json_md: Path, *, previous=None):
108122
previous = previous or []
109123

110124
yield from previous
111-
# for record in previous:
112-
# yield record
113-
# yield (
114-
# record.get('saved_on'),
115-
# record.get('current_loc'),
116-
# record.get('uri'),
117-
# record.get('modified'),
118-
# record.get('size'),
119-
# record.get('hash')
120-
# )
125+
126+
127+
def load_metadata(json_md: Path, *, meta=None) -> list:
128+
"""Retreive last backup metadata from file and
129+
returns the array of dictionaries or an empty array"""
130+
131+
try:
132+
with open(json_md) as json_in:
133+
meta = json.loads(json_in.read())
134+
except FileNotFoundError:
135+
logger.warning('Unable to locate metadata file. Creating new file')
136+
except json.JSONDecodeError:
137+
logger.warning('Unable to decode json in metadata file')
138+
finally:
139+
meta = meta or []
140+
return meta
141+
121142

122143

123144
def check_for_deleted(current: set, previous: set) -> list[SnFile]:
@@ -192,7 +213,6 @@ def backup() -> None:
192213
raise SystemExit()
193214

194215
if args.setup:
195-
from .setup import SetupConf # Lazy import
196216
setup = SetupConf()
197217
setup.prompt()
198218
setup.write_config()

snbackup/files.py

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import os
12
from pathlib import Path
23
from hashlib import sha256
4+
from typing import NamedTuple
5+
from dataclasses import dataclass
36
from datetime import datetime, date
47
from functools import total_ordering
58

@@ -12,37 +15,35 @@ class BadDateError(Exception):
1215
"""Bad datetime format or value"""
1316

1417

18+
class FileMeta(NamedTuple):
19+
uri: str
20+
modified: str
21+
size: int
22+
saved: str | None = None
23+
current_loc: str | None = None
24+
prev_hash: str | None = None
25+
26+
1527
@total_ordering
1628
class SnFile:
1729
"""Represent an individual Supernote file"""
1830

19-
def __init__(
20-
self, save_to: Path,
21-
today: date,
22-
uri: str,
23-
modified: str,
24-
size: int,
25-
saved: str | None = None,
26-
current_loc: str | None = None,
27-
prev_hash: str | None = None
28-
) -> None:
31+
def __init__(self, save_to: Path, today: date, file_meta: FileMeta) -> None:
2932
self.save_to = save_to
3033
self.today = today
31-
self.file_uri = uri
32-
self.last_modified = modified
33-
self.size = size
34-
self.saved = saved
35-
self.current_loc = current_loc
36-
self.prev_hash = prev_hash
34+
self.file_meta = file_meta
3735
self._file_bytes = b''
36+
self._calc_hash = None
37+
self._consistency = None
3838

3939
@property
4040
def save_date(self) -> str:
4141
return self.base_path.name
4242

4343
@property
4444
def full_path(self) -> Path:
45-
return self.base_path.joinpath(self.uri)
45+
date_pth = Path(f'{self.date!s}/{self.uri}')
46+
return self.save_to.joinpath(date_pth)
4647

4748
@property
4849
def file_bytes(self) -> bytes:
@@ -80,13 +81,20 @@ def _calc_hash(self) -> str:
8081
return ''
8182
return sha256(self._file_bytes).hexdigest()
8283

84+
def _validate(self) -> None:
85+
if self.prev_hash:
86+
self._consistency = (self._file_hash == self.prev_hash)
87+
8388
def _make_dir(self) -> None:
8489
self.full_path.parent.mkdir(exist_ok=True, parents=True)
8590

8691
def save_file(self) -> None:
92+
"""More verbose write operation to ensure file is written to disk"""
8793
self._make_dir()
8894
with open(self.full_path, 'wb') as file_output:
8995
file_output.write(self.file_bytes)
96+
file_output.flush()
97+
os.fsync(file_output.fileno())
9098

9199
def __eq__(self, other) -> bool:
92100
if not isinstance(other, self.__class__):
@@ -101,8 +109,27 @@ def __lt__(self, other) -> bool:
101109
def __hash__(self) -> int:
102110
return hash((self.uri, self.modified, self.size))
103111

112+
# def __repr__(self) -> str:
113+
# return f"{type(self).__name__}({self.base_path!r}, {self.uri!r}, '{self.modified}', {self.size})"
114+
104115
def __repr__(self) -> str:
105-
return f"{type(self).__name__}({self.base_path!r}, {self.uri!r}, '{self.modified}', {self.size})"
116+
return (
117+
f'{type(self).__name__}('
118+
f'save_to={self.save_to!r}, '
119+
f'uri={self.uri!r}, '
120+
f"modified='{self.modified}', "
121+
f'size={self.size}), '
122+
f''
123+
)
124+
125+
# def __repr__(self) -> str:
126+
# return f"""{type(self).__name__}(
127+
# base_path={self.base_path!r},
128+
# uri={self.uri!r},
129+
# modified='{self.modified}',
130+
# size={self.size}
131+
# )"""
132+
106133

107134
def make_record(self) -> dict:
108135
return {

snbackup/helpers.py

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,55 @@
77
from .setup import SetupConf
88

99
FOLDERS = {
10-
'note': 'Note',
11-
'document': 'Document',
12-
'export': 'EXPORT',
13-
'mystyle': 'MyStyle',
14-
'screenshot': 'SCREENSHOT',
10+
'note': 'Note',
11+
'document': 'Document',
12+
'export': 'EXPORT',
13+
'mystyle': 'MyStyle',
14+
'screenshot': 'SCREENSHOT',
1515
'inbox': 'INBOX',
1616
}
1717

1818
EXTS = {
19-
'.note', '.pdf', '.epub', '.docx', '.doc',
20-
'.txt', '.png', '.jpg', '.jpeg', '.bmp',
21-
'.webp', '.cbz', '.fb2', '.xps', '.mobi',
19+
'.note',
20+
'.pdf',
21+
'.epub',
22+
'.docx',
23+
'.doc',
24+
'.txt',
25+
'.png',
26+
'.jpg',
27+
'.jpeg',
28+
'.bmp',
29+
'.webp',
30+
'.cbz',
31+
'.fb2',
32+
'.xps',
33+
'.mobi',
2234
}
2335

2436

2537
def user_input() -> Namespace:
2638
parser = ArgumentParser(allow_abbrev=False)
2739
parser.add_argument('-c', '--config', type=Path, help='Path to config.json file')
28-
parser.add_argument('-f', '--full', action='store_true', help='Download all notes and files from device. Disregard any saved locally.')
29-
parser.add_argument('-i', '--inspect', action='store_true', help='Inspect device for new files to download and quit')
30-
parser.add_argument('-u', '--upload', nargs='+', help='Send one or more files to device. "snbackup -u file1 file2 file3"')
3140
parser.add_argument(
32-
'-d', '--destination', default='document', type=str.lower, choices=FOLDERS, help='Destination folder to send file upload'
41+
'-f',
42+
'--full',
43+
action='store_true',
44+
help='Download all notes and files from device. Disregard any saved locally.',
45+
)
46+
parser.add_argument(
47+
'-i', '--inspect', action='store_true', help='Inspect device for new files to download and quit'
48+
)
49+
parser.add_argument(
50+
'-u', '--upload', nargs='+', help='Send one or more files to device. "snbackup -u file1 file2 file3"'
51+
)
52+
parser.add_argument(
53+
'-d',
54+
'--destination',
55+
default='document',
56+
type=str.casefold,
57+
choices=FOLDERS,
58+
help='Destination folder to send file upload',
3359
)
3460
parser.add_argument('-ls', '--list', action='store_true', help='List out information about backups found locally')
3561
parser.add_argument('-v', '--version', action='store_true', help='Print program version and quit.')
@@ -53,11 +79,7 @@ def user_input() -> Namespace:
5379

5480
def locate_config() -> Path:
5581
"""Look in potential config locations and return first valid"""
56-
conf_locations = (
57-
os.getenv('SNBACKUP_CONF', ''),
58-
SetupConf.home_conf,
59-
Path().cwd().joinpath(SetupConf.config)
60-
)
82+
conf_locations = (os.getenv('SNBACKUP_CONF', ''), SetupConf.home_conf, Path().cwd().joinpath(SetupConf.config))
6183
for conf in conf_locations:
6284
pth = Path(conf)
6385
if pth.is_file() and pth.suffix == '.json':
@@ -84,7 +106,8 @@ def today_pth(save_dir: Path) -> Path:
84106

85107
def check_version(name: str) -> str:
86108
from importlib.metadata import version
87-
return f"{name} v{version(name)}"
109+
110+
return f'{name} v{version(name)}'
88111

89112

90113
def bytes_to_mb(byte_size: int) -> str:
@@ -93,7 +116,7 @@ def bytes_to_mb(byte_size: int) -> str:
93116

94117

95118
def count_backups(directory: Path, pattern='202?-*') -> tuple[int, Path, Path]:
96-
"""Counts number of backup folders and returns oldest and
119+
"""Counts number of backup folders and returns oldest and
97120
newest found on local disk"""
98121

99122
previous = sorted(directory.glob(pattern))

0 commit comments

Comments
 (0)