aboutsummaryrefslogtreecommitdiffstats
path: root/tg_backup.py
diff options
context:
space:
mode:
Diffstat (limited to 'tg_backup.py')
-rw-r--r--tg_backup.py40
1 files changed, 38 insertions, 2 deletions
diff --git a/tg_backup.py b/tg_backup.py
index 382a756..779d76e 100644
--- a/tg_backup.py
+++ b/tg_backup.py
@@ -144,6 +144,15 @@ def parse_target(target):
return int(t)
return t
+def same_target(a, b):
+ # Only catches targets that are textually the same chat. "@foo" and "foo"
+ # match; "@foo" and its numeric ID do not, since resolving them needs a
+ # logged-in client and the check runs before that.
+ if a is None or b is None:
+ return True # nothing saved to contradict
+ norm = lambda t: str(t).strip().lstrip('@').lower()
+ return norm(a) == norm(b)
+
def make_client(archive_dir):
# One session in CONFIG_DIR, shared by every archive dir: log in once, back
# up any number of chats.
@@ -189,7 +198,19 @@ async def list_chats(archive_dir):
await client.disconnect()
print("\nBack up with: --target=<ID> (the = matters for negative IDs)")
-async def run_backup(target, archive_dir):
+async def run_backup(target, archive_dir, force_target=False):
+ # last_id is a high-water mark for one specific chat. Applied as min_id to a
+ # different one it silently skips that chat's history, and the run reports
+ # success. Refuse before touching the network.
+ saved = load_state(archive_dir).get('target')
+ if not force_target and not same_target(saved, target):
+ sys.exit(f"Archive dir {archive_dir} was last backed up with target "
+ f"'{saved}', not '{target}'.\n"
+ "Its saved resume position belongs to that chat and would skip "
+ "this one's history. Use a separate archive dir per chat, or "
+ "pass --force-target to overwrite (only correct if the same "
+ "chat changed handle or ID).")
+
client = make_client(archive_dir)
await start_client(client)
@@ -251,6 +272,9 @@ def main():
help="Directory to store media and state.")
parser.add_argument("--list-chats", action="store_true",
help="List your dialogs with their IDs and exit.")
+ parser.add_argument("--force-target", action="store_true",
+ help="Back up --target into an archive dir saved with a "
+ "different one, keeping its resume position.")
args = parser.parse_args()
if args.list_chats:
@@ -259,7 +283,7 @@ def main():
# An archive dir already backed up once knows its own target.
target = args.target or load_state(args.archive_dir).get('target')
if target:
- asyncio.run(run_backup(target, args.archive_dir))
+ asyncio.run(run_backup(target, args.archive_dir, args.force_target))
else:
parser.error(f"--target is required the first time ({args.archive_dir}/state.json "
"has no saved target), or use --list-chats")
@@ -308,6 +332,18 @@ def _self_check():
assert load_state(archive).get('target') == '-1001234567890'
assert load_state(Path(tempfile.mkdtemp())).get('target') is None
+ # A dir keeps its chat. Handle case and the @ prefix are noise, a different
+ # chat is not. Nothing saved yet must never block a first run.
+ assert same_target('@examplegroup', 'examplegroup')
+ assert same_target('@ExampleGroup', ' @examplegroup ')
+ assert same_target('-1001234567890', '-1001234567890')
+ assert same_target(-1001234567890, '-1001234567890') # state may hold an int
+ assert same_target(None, '@examplegroup') # first run, nothing saved
+ assert same_target('@examplegroup', None) # resumed run, no --target given
+ assert not same_target('@examplegroup', '@othergroup')
+ assert not same_target('-1001234567890', '-1009876543210')
+ assert not same_target('@examplegroup', '-1001234567890') # unresolvable, refuse
+
# Failures append, one parseable record per line, and survive an exception
# object being passed straight in.
record_failure(archive, 11, '11_a.jpg', OSError('disk full'))