From 4fad48721b94762c092ea8c3d2651e8d771b62ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Somogyi?= Date: Sat, 11 Feb 2023 07:05:13 +0100 Subject: [PATCH 01/12] enh: start contacts --- .../providers/contacts_service_provider.py | 13 +++++ .../contacts_service_wrapper_interface.py | 12 +++++ .../gapi_contacts_service_wrapper.py | 47 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 gwbackupy/providers/contacts_service_provider.py create mode 100644 gwbackupy/providers/contacts_service_wrapper_interface.py create mode 100644 gwbackupy/providers/gapi_contacts_service_wrapper.py diff --git a/gwbackupy/providers/contacts_service_provider.py b/gwbackupy/providers/contacts_service_provider.py new file mode 100644 index 0000000..53df0fc --- /dev/null +++ b/gwbackupy/providers/contacts_service_provider.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from gwbackupy.providers.gapi_service_provider import GapiServiceProvider +from gwbackupy.storage.storage_interface import StorageInterface + + +class ContactsServiceProvider(GapiServiceProvider): + """Contacts service provider from gmail/v1 API with full access scope""" + + def __init__(self, **kwargs): + super(ContactsServiceProvider, self).__init__( + "people", "v1", ["https://www.googleapis.com/auth/contacts"], **kwargs + ) diff --git a/gwbackupy/providers/contacts_service_wrapper_interface.py b/gwbackupy/providers/contacts_service_wrapper_interface.py new file mode 100644 index 0000000..deacc32 --- /dev/null +++ b/gwbackupy/providers/contacts_service_wrapper_interface.py @@ -0,0 +1,12 @@ +from __future__ import annotations + + +class ContactsServiceWrapperInterface: + def get_contacts(self, email: str) -> dict[str, [dict[str, any]]]: + pass + + def get_contact(self, email: str, contact_id: str) -> [dict[str, any]]: + pass + + def get_contact_main_photo(self, email: str, contact_id: str) -> bytes: + pass diff --git a/gwbackupy/providers/gapi_contacts_service_wrapper.py b/gwbackupy/providers/gapi_contacts_service_wrapper.py new file mode 100644 index 0000000..1f24777 --- /dev/null +++ b/gwbackupy/providers/gapi_contacts_service_wrapper.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from gwbackupy.providers.contacts_service_provider import ContactsServiceProvider +from gwbackupy.providers.contacts_service_wrapper_interface import ( + ContactsServiceWrapperInterface, +) + + +class GapiContactsServiceWrapper(ContactsServiceWrapperInterface): + def __init__( + self, + service_provider: ContactsServiceProvider, + try_count: int = 5, + try_sleep: int = 10, + dry_mode: bool = False, + ): + self.try_count = try_count + self.try_sleep = try_sleep + self.service_provider = service_provider + self.dry_mode = dry_mode + + def get_service_provider(self) -> ContactsServiceProvider: + return self.service_provider + + def get_contacts(self, email: str) -> dict[str, [dict[str, any]]]: + with self.service_provider.get_service(email) as service: + response = ( + service.people() + .connections() + .list( + resourceName="people/me", + pageSize=1500, + personFields="addresses,ageRange,biographies,birthdays,braggingRights,coverPhotos,emailAddresses," + "events,genders,imClients,interests,locales,memberships,metadata,names,nicknames," + "occupations,organizations,phoneNumbers,photos,relations,relationshipInterests," + "relationshipStatuses,residences,skills,taglines,urls,userDefined", + ) + .execute() + ) + print(response) + exit(1) + + def get_contact(self, email: str, contact_id: str) -> [dict[str, any]]: + pass + + def get_contact_main_photo(self, email: str, contact_id: str) -> bytes: + pass From d0577463a599ae59bb104758db30ec2e78888683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Somogyi?= Date: Tue, 14 Feb 2023 03:57:54 +0100 Subject: [PATCH 02/12] remove unused variable --- gwbackupy/gmail.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gwbackupy/gmail.py b/gwbackupy/gmail.py index 0641388..f0faa3a 100644 --- a/gwbackupy/gmail.py +++ b/gwbackupy/gmail.py @@ -51,7 +51,6 @@ def __init__( batch_size = 5 self.batch_size = batch_size self.__lock = threading.RLock() - self.__services = {} self.__error_count = 0 self.__service_wrapper = service_wrapper if labels is None: From c71cd2811d06481616b86754ba1eca350127e262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Somogyi?= Date: Tue, 14 Feb 2023 04:28:50 +0100 Subject: [PATCH 03/12] people backup --- docs/enable-gcp-apis.md | 17 ++- docs/images/people-api-enable.png | Bin 0 -> 36300 bytes docs/images/people-api-enabled.png | Bin 0 -> 61776 bytes gwbackupy/gwbackupy_cli.py | 32 ++++ gwbackupy/people.py | 140 ++++++++++++++++++ .../contacts_service_wrapper_interface.py | 12 -- .../gapi_contacts_service_wrapper.py | 47 ------ .../providers/gapi_people_service_wrapper.py | 66 +++++++++ ...provider.py => people_service_provider.py} | 4 +- .../people_service_wrapper_interface.py | 12 ++ 10 files changed, 268 insertions(+), 62 deletions(-) create mode 100644 docs/images/people-api-enable.png create mode 100644 docs/images/people-api-enabled.png create mode 100644 gwbackupy/people.py delete mode 100644 gwbackupy/providers/contacts_service_wrapper_interface.py delete mode 100644 gwbackupy/providers/gapi_contacts_service_wrapper.py create mode 100644 gwbackupy/providers/gapi_people_service_wrapper.py rename gwbackupy/providers/{contacts_service_provider.py => people_service_provider.py} (78%) create mode 100644 gwbackupy/providers/people_service_wrapper_interface.py diff --git a/docs/enable-gcp-apis.md b/docs/enable-gcp-apis.md index a82cbee..d26f183 100644 --- a/docs/enable-gcp-apis.md +++ b/docs/enable-gcp-apis.md @@ -16,4 +16,19 @@ If the process was successful, you will see the following screen: ![](images/gmail-api-enabled.png) -The Gmail API is enabled. \ No newline at end of file +The Gmail API is enabled. + +## People API (for contacts) + +Navigate to this link: +https://console.cloud.google.com/marketplace/product/google/people.googleapis.com + +Click to "ENABLE" button. + +![](images/people-api-enable.png) + +If the process was successful, you will see the following screen: + +![](images/people-api-enabled.png) + +The People API is enabled. diff --git a/docs/images/people-api-enable.png b/docs/images/people-api-enable.png new file mode 100644 index 0000000000000000000000000000000000000000..033b19ae249ff89b5c146ae5ede9737e25541361 GIT binary patch literal 36300 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 literal 0 HcmV?d00001 diff --git a/docs/images/people-api-enabled.png b/docs/images/people-api-enabled.png new file mode 100644 index 0000000000000000000000000000000000000000..3142c4311f6096c69deb0162116925b9499506d1 GIT binary patch literal 61776 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

pc5^*!X{+DHnr{ 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 Uo+WAJ_#*Ilpz*G}pME|1FLgahO8@`> literal 0 HcmV?d00001 diff --git a/gwbackupy/gwbackupy_cli.py b/gwbackupy/gwbackupy_cli.py index e376128..5407bbf 100644 --- a/gwbackupy/gwbackupy_cli.py +++ b/gwbackupy/gwbackupy_cli.py @@ -10,6 +10,9 @@ from gwbackupy.filters.gmail_filter import GmailFilter from gwbackupy.gmail import Gmail from gwbackupy.helpers import parse_date +from gwbackupy.people import People +from gwbackupy.providers.people_service_provider import PeopleServiceProvider +from gwbackupy.providers.gapi_people_service_wrapper import GapiPeopleServiceWrapper from gwbackupy.providers.gapi_gmail_service_wrapper import GapiGmailServiceWrapper from gwbackupy.providers.gapi_service_provider import AccessNotInitializedError from gwbackupy.providers.gmail_service_provider import GmailServiceProvider @@ -204,6 +207,35 @@ def parse_arguments() -> argparse.Namespace: def cli_startup(): try: args = parse_arguments() + + storage = FileStorage(args.workdir + "/" + args.email + "/peoples") + storage_oauth_tokens = FileStorage(args.workdir + "/oauth-tokens") + service_provider = PeopleServiceProvider( + credentials_file_path=args.credentials_filepath, + service_account_email=args.service_account_email, + service_account_file_path=args.service_account_key_filepath, + storage=storage_oauth_tokens, + oauth_bind_addr=args.oauth_bind_address, + oauth_port=args.oauth_port, + oauth_redirect_host=args.oauth_redirect_host, + ) + service_wrapper = GapiPeopleServiceWrapper( + service_provider=service_provider, + dry_mode=args.dry, + ) + + service = People( + email=args.email, + service_wrapper=service_wrapper, + # batch_size=args.batch_size, + batch_size=1, + storage=storage, + dry_mode=args.dry, + ) + + service.backup() + exit(1) + if args.service == "gmail": storage = FileStorage(args.workdir + "/" + args.email + "/gmail") storage_oauth_tokens = FileStorage(args.workdir + "/oauth-tokens") diff --git a/gwbackupy/people.py b/gwbackupy/people.py new file mode 100644 index 0000000..788df0c --- /dev/null +++ b/gwbackupy/people.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +import concurrent +import json +import logging +import threading + +from gwbackupy import global_properties +from gwbackupy.process_helpers import await_all_futures +from gwbackupy.providers.people_service_wrapper_interface import ( + PeopleServiceWrapperInterface, +) +from gwbackupy.storage.storage_interface import StorageInterface, LinkInterface, Data + + +class People: + """People (contacts) service""" + + def __init__( + self, + email: str, + storage: StorageInterface, + service_wrapper: PeopleServiceWrapperInterface, + batch_size: int = 10, + dry_mode: bool = False, + ): + self.dry_mode = dry_mode + self.email = email + self.storage = storage + if batch_size is None or batch_size < 1: + batch_size = 5 + self.batch_size = batch_size + self.__lock = threading.RLock() + self.__error_count = 0 + self.__service_wrapper = service_wrapper + + def backup(self): + logging.info(f"Starting backup for {self.email}") + self.__error_count = 0 + + logging.info("Scanning backup storage...") + stored_data_all = self.storage.find() + logging.info(f"Stored items: {len(stored_data_all)}") + + stored_items: dict[str, dict[int, LinkInterface]] = stored_data_all.find( + f=lambda l: not l.is_special_id() and (l.is_metadata() or l.is_object()), + g=lambda l: [l.id(), 0 if l.is_metadata() else 1], + ) + + del stored_data_all + for item_id in list(stored_items.keys()): + link_metadata = stored_items[item_id].get(0) + if link_metadata is None: + logging.error(f"{item_id} metadata is not found in locally") + del stored_items[item_id] + elif link_metadata.is_deleted(): + logging.debug(f"{item_id} metadata is already deleted") + del stored_items[item_id] + else: + logging.log( + global_properties.log_finest, + f"{item_id} is usable from backup storage", + ) + logging.info(f"Stored peoples: {len(stored_items)}") + + items_from_server = self.__service_wrapper.get_peoples(self.email) + + logging.info("Processing...") + executor = concurrent.futures.ThreadPoolExecutor(max_workers=self.batch_size) + futures = [] + # submit message download jobs + for message_id in items_from_server: + futures.append( + executor.submit( + self.__backup_item, + items_from_server[message_id], + stored_items, + ) + ) + # wait for jobs + if not await_all_futures(futures): + # cancel jobs + executor.shutdown(cancel_futures=True) + logging.warning("Process is killed") + return False + logging.info("Processed") + + if self.__error_count > 0: + # if error then never delete! + logging.error("Backup failed with " + str(self.__error_count) + " errors") + return False + + return False + + def __backup_item( + self, people: dict[str, any], stored_items: dict[str, dict[int, LinkInterface]] + ): + people_id = people.get("resourceName", "UNKNOWN") # for logging + try: + people_id = people["resourceName"] + latest_meta_link = None + if people_id in stored_items: + latest_meta_link = stored_items[people_id][0] + is_new = latest_meta_link is None + if is_new: + logging.debug(f"{people_id} is new") + + write_meta = True # if any failure then write it force + + # ... + + if write_meta: + link = self.storage.new_link( + object_id=people_id, + extension="json", + created_timestamp=0.0, + ).set_properties({LinkInterface.property_metadata: True}) + success = self.__storage_put(link, data=json.dumps(people)) + if success: + logging.info(f"{people_id} meta data is saved") + else: + raise Exception("Meta data put failed") + else: + logging.debug(f"{people_id} meta data is not changed, skip put") + + except Exception as e: + with self.__lock: + self.__error_count += 1 + if str(e) == "SKIP": + return + logging.exception(f"{people_id} {e}") + + def __storage_put(self, link: LinkInterface, data: Data) -> bool: + if self.dry_mode: + logging.info(f"DRY MODE storage put: {link}") + return True + return self.storage.put(link, data) + + def restore(self): + pass diff --git a/gwbackupy/providers/contacts_service_wrapper_interface.py b/gwbackupy/providers/contacts_service_wrapper_interface.py deleted file mode 100644 index deacc32..0000000 --- a/gwbackupy/providers/contacts_service_wrapper_interface.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import annotations - - -class ContactsServiceWrapperInterface: - def get_contacts(self, email: str) -> dict[str, [dict[str, any]]]: - pass - - def get_contact(self, email: str, contact_id: str) -> [dict[str, any]]: - pass - - def get_contact_main_photo(self, email: str, contact_id: str) -> bytes: - pass diff --git a/gwbackupy/providers/gapi_contacts_service_wrapper.py b/gwbackupy/providers/gapi_contacts_service_wrapper.py deleted file mode 100644 index 1f24777..0000000 --- a/gwbackupy/providers/gapi_contacts_service_wrapper.py +++ /dev/null @@ -1,47 +0,0 @@ -from __future__ import annotations - -from gwbackupy.providers.contacts_service_provider import ContactsServiceProvider -from gwbackupy.providers.contacts_service_wrapper_interface import ( - ContactsServiceWrapperInterface, -) - - -class GapiContactsServiceWrapper(ContactsServiceWrapperInterface): - def __init__( - self, - service_provider: ContactsServiceProvider, - try_count: int = 5, - try_sleep: int = 10, - dry_mode: bool = False, - ): - self.try_count = try_count - self.try_sleep = try_sleep - self.service_provider = service_provider - self.dry_mode = dry_mode - - def get_service_provider(self) -> ContactsServiceProvider: - return self.service_provider - - def get_contacts(self, email: str) -> dict[str, [dict[str, any]]]: - with self.service_provider.get_service(email) as service: - response = ( - service.people() - .connections() - .list( - resourceName="people/me", - pageSize=1500, - personFields="addresses,ageRange,biographies,birthdays,braggingRights,coverPhotos,emailAddresses," - "events,genders,imClients,interests,locales,memberships,metadata,names,nicknames," - "occupations,organizations,phoneNumbers,photos,relations,relationshipInterests," - "relationshipStatuses,residences,skills,taglines,urls,userDefined", - ) - .execute() - ) - print(response) - exit(1) - - def get_contact(self, email: str, contact_id: str) -> [dict[str, any]]: - pass - - def get_contact_main_photo(self, email: str, contact_id: str) -> bytes: - pass diff --git a/gwbackupy/providers/gapi_people_service_wrapper.py b/gwbackupy/providers/gapi_people_service_wrapper.py new file mode 100644 index 0000000..a599442 --- /dev/null +++ b/gwbackupy/providers/gapi_people_service_wrapper.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import logging + +from gwbackupy.providers.people_service_provider import PeopleServiceProvider +from gwbackupy.providers.people_service_wrapper_interface import ( + PeopleServiceWrapperInterface, +) + + +class GapiPeopleServiceWrapper(PeopleServiceWrapperInterface): + def __init__( + self, + service_provider: PeopleServiceProvider, + try_count: int = 5, + try_sleep: int = 10, + dry_mode: bool = False, + ): + self.try_count = try_count + self.try_sleep = try_sleep + self.service_provider = service_provider + self.dry_mode = dry_mode + + def get_service_provider(self) -> PeopleServiceProvider: + return self.service_provider + + def get_peoples(self, email: str) -> dict[str, [dict[str, any]]]: + with self.service_provider.get_service(email) as service: + next_page_token = None + page = 1 + items: dict[str, [dict[str, any]]] = dict() + while True: + logging.debug(f"Loading page {page}. from server...") + data = ( + service.people() + .connections() + .list( + resourceName="people/me", + pageSize=50, + pageToken=next_page_token, + personFields="addresses,ageRange,biographies,birthdays,braggingRights,coverPhotos," + "events,genders,imClients,interests,locales,memberships,metadata,names,nicknames," + "emailAddresses,occupations,organizations,phoneNumbers,photos,relations,relationshipInterests," + "relationshipStatuses,residences,skills,taglines,urls,userDefined", + ) + .execute() + ) + print(data) + next_page_token = data.get("nextPageToken", None) + page_message_count = len(data.get("connections", [])) + logging.debug( + f"Page {page} successfully loaded (connections count: {page_message_count} / next page token: {next_page_token})" + ) + for item in data.get("connections", []): + items[item.get("resourceName")] = item + page += 1 + if next_page_token is None: + break + print(items) + return items + + def get_people(self, email: str, contact_id: str) -> [dict[str, any]]: + pass + + def get_people_main_photo(self, email: str, contact_id: str) -> bytes: + pass diff --git a/gwbackupy/providers/contacts_service_provider.py b/gwbackupy/providers/people_service_provider.py similarity index 78% rename from gwbackupy/providers/contacts_service_provider.py rename to gwbackupy/providers/people_service_provider.py index 53df0fc..6208ef9 100644 --- a/gwbackupy/providers/contacts_service_provider.py +++ b/gwbackupy/providers/people_service_provider.py @@ -4,10 +4,10 @@ from gwbackupy.storage.storage_interface import StorageInterface -class ContactsServiceProvider(GapiServiceProvider): +class PeopleServiceProvider(GapiServiceProvider): """Contacts service provider from gmail/v1 API with full access scope""" def __init__(self, **kwargs): - super(ContactsServiceProvider, self).__init__( + super(PeopleServiceProvider, self).__init__( "people", "v1", ["https://www.googleapis.com/auth/contacts"], **kwargs ) diff --git a/gwbackupy/providers/people_service_wrapper_interface.py b/gwbackupy/providers/people_service_wrapper_interface.py new file mode 100644 index 0000000..2dd1b56 --- /dev/null +++ b/gwbackupy/providers/people_service_wrapper_interface.py @@ -0,0 +1,12 @@ +from __future__ import annotations + + +class PeopleServiceWrapperInterface: + def get_peoples(self, email: str) -> dict[str, [dict[str, any]]]: + pass + + def get_people(self, email: str, contact_id: str) -> [dict[str, any]]: + pass + + def get_people_main_photo(self, email: str, contact_id: str) -> bytes: + pass From 656225a805ea82c0defc139a5e671e36d2cbd2fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Somogyi?= Date: Sat, 18 Feb 2023 04:08:53 +0100 Subject: [PATCH 04/12] code refactoring --- gwbackupy/__main__.py | 2 +- gwbackupy/cli/gmail_cli.py | 71 ++++++++++++ gwbackupy/{ => cli}/gwbackupy_cli.py | 167 +++++++++------------------ gwbackupy/cli/peoples_cli.py | 56 +++++++++ setup.py | 1 + 5 files changed, 182 insertions(+), 115 deletions(-) create mode 100644 gwbackupy/cli/gmail_cli.py rename gwbackupy/{ => cli}/gwbackupy_cli.py (61%) create mode 100644 gwbackupy/cli/peoples_cli.py diff --git a/gwbackupy/__main__.py b/gwbackupy/__main__.py index 92f0f43..9ebb69e 100644 --- a/gwbackupy/__main__.py +++ b/gwbackupy/__main__.py @@ -1,4 +1,4 @@ -from gwbackupy import gwbackupy_cli +from gwbackupy.cli import gwbackupy_cli if __name__ == "__main__": gwbackupy_cli.cli_startup() diff --git a/gwbackupy/cli/gmail_cli.py b/gwbackupy/cli/gmail_cli.py new file mode 100644 index 0000000..54d3acb --- /dev/null +++ b/gwbackupy/cli/gmail_cli.py @@ -0,0 +1,71 @@ +def add_cli_args_gmail(service_parser): + gmail_parser = service_parser.add_parser("gmail", help="GMail service commands") + gmail_command_parser = gmail_parser.add_subparsers(dest="command") + + gmail_oauth_init_parser = gmail_command_parser.add_parser( + "access-init", help="Access initialization e.g. OAuth authentication" + ) + gmail_oauth_init_parser.add_argument( + "--email", type=str, help="Email account", required=True + ) + gmail_oauth_check_parser = gmail_command_parser.add_parser( + "access-check", help="Check access e.g. OAuth tokens" + ) + gmail_oauth_check_parser.add_argument( + "--email", type=str, help="Email account", required=True + ) + gmail_backup_parser = gmail_command_parser.add_parser("backup", help="Backup gmail") + gmail_backup_parser.add_argument( + "--email", type=str, help="Email of the account", required=True + ) + gmail_backup_parser.add_argument( + "--quick-sync-days", + type=int, + default=None, + help="Quick sync number of days back. (It does not delete messages from local " + "storage.)", + ) + + gmail_restore_parser = gmail_command_parser.add_parser( + "restore", help="Restore gmail" + ) + gmail_restore_parser.add_argument( + "--email", type=str, help="Email from which restore", required=True + ) + gmail_restore_parser.add_argument( + "--to-email", + type=str, + help="Destination email account, if not specified, then --email is used", + ) + gmail_restore_parser.add_argument( + "--add-label", + type=str, + action="append", + help="Add label to restored emails", + default=None, + dest="add_labels", + ) + gmail_restore_parser.add_argument( + "--restore-deleted", + help="Restore deleted emails", + default=False, + action="store_true", + ) + gmail_restore_parser.add_argument( + "--restore-missing", + help="Restore missing emails", + default=False, + action="store_true", + ) + gmail_restore_parser.add_argument( + "--filter-date-from", + type=str, + help="Filter date from (inclusive, format: yyyy-mm-dd or yyyy-mm-dd hh:mm:ss)", + default=None, + ) + gmail_restore_parser.add_argument( + "--filter-date-to", + type=str, + help="Filter date to (exclusive, format: yyyy-mm-dd or yyyy-mm-dd hh:mm:ss)", + default=None, + ) diff --git a/gwbackupy/gwbackupy_cli.py b/gwbackupy/cli/gwbackupy_cli.py similarity index 61% rename from gwbackupy/gwbackupy_cli.py rename to gwbackupy/cli/gwbackupy_cli.py index 5407bbf..9c37838 100644 --- a/gwbackupy/gwbackupy_cli.py +++ b/gwbackupy/cli/gwbackupy_cli.py @@ -1,5 +1,6 @@ import argparse import logging +import os import sys import threading @@ -7,21 +8,23 @@ from tzlocal import get_localzone import gwbackupy.global_properties as global_properties +from gwbackupy.cli.gmail_cli import add_cli_args_gmail +from gwbackupy.cli.peoples_cli import add_cli_args_peoples from gwbackupy.filters.gmail_filter import GmailFilter from gwbackupy.gmail import Gmail from gwbackupy.helpers import parse_date from gwbackupy.people import People -from gwbackupy.providers.people_service_provider import PeopleServiceProvider -from gwbackupy.providers.gapi_people_service_wrapper import GapiPeopleServiceWrapper from gwbackupy.providers.gapi_gmail_service_wrapper import GapiGmailServiceWrapper +from gwbackupy.providers.gapi_people_service_wrapper import GapiPeopleServiceWrapper from gwbackupy.providers.gapi_service_provider import AccessNotInitializedError from gwbackupy.providers.gmail_service_provider import GmailServiceProvider +from gwbackupy.providers.people_service_provider import PeopleServiceProvider from gwbackupy.storage.file_storage import FileStorage lock = threading.Lock() -def parse_arguments() -> argparse.Namespace: +def parse_arguments(people_cli=None) -> argparse.Namespace: log_levels = { "finest": global_properties.log_finest, "debug": logging.DEBUG, @@ -107,77 +110,9 @@ def parse_arguments() -> argparse.Namespace: help="OAuth redirect host, default is localhost", ) service_parser = parser.add_subparsers(dest="service") - gmail_parser = service_parser.add_parser("gmail", help="GMail service commands") - gmail_command_parser = gmail_parser.add_subparsers(dest="command") - - gmail_oauth_init_parser = gmail_command_parser.add_parser( - "access-init", help="Access initialization e.g. OAuth authentication" - ) - gmail_oauth_init_parser.add_argument( - "--email", type=str, help="Email account", required=True - ) - gmail_oauth_check_parser = gmail_command_parser.add_parser( - "access-check", help="Check access e.g. OAuth tokens" - ) - gmail_oauth_check_parser.add_argument( - "--email", type=str, help="Email account", required=True - ) - - gmail_backup_parser = gmail_command_parser.add_parser("backup", help="Backup gmail") - gmail_backup_parser.add_argument( - "--email", type=str, help="Email of the account", required=True - ) - gmail_backup_parser.add_argument( - "--quick-sync-days", - type=int, - default=None, - help="Quick sync number of days back. (It does not delete messages from local " - "storage.)", - ) + add_cli_args_gmail(service_parser) + add_cli_args_peoples(service_parser) - gmail_restore_parser = gmail_command_parser.add_parser( - "restore", help="Restore gmail" - ) - gmail_restore_parser.add_argument( - "--email", type=str, help="Email from which restore", required=True - ) - gmail_restore_parser.add_argument( - "--to-email", - type=str, - help="Destination email account, if not specified, then --email is used", - ) - gmail_restore_parser.add_argument( - "--add-label", - type=str, - action="append", - help="Add label to restored emails", - default=None, - dest="add_labels", - ) - gmail_restore_parser.add_argument( - "--restore-deleted", - help="Restore deleted emails", - default=False, - action="store_true", - ) - gmail_restore_parser.add_argument( - "--restore-missing", - help="Restore missing emails", - default=False, - action="store_true", - ) - gmail_restore_parser.add_argument( - "--filter-date-from", - type=str, - help="Filter date from (inclusive, format: yyyy-mm-dd or yyyy-mm-dd hh:mm:ss)", - default=None, - ) - gmail_restore_parser.add_argument( - "--filter-date-to", - type=str, - help="Filter date to (exclusive, format: yyyy-mm-dd or yyyy-mm-dd hh:mm:ss)", - default=None, - ) if len(sys.argv) == 1 or "--help" in sys.argv: parser.print_help(sys.stderr) sys.exit(1) @@ -208,51 +143,55 @@ def cli_startup(): try: args = parse_arguments() - storage = FileStorage(args.workdir + "/" + args.email + "/peoples") - storage_oauth_tokens = FileStorage(args.workdir + "/oauth-tokens") - service_provider = PeopleServiceProvider( - credentials_file_path=args.credentials_filepath, - service_account_email=args.service_account_email, - service_account_file_path=args.service_account_key_filepath, - storage=storage_oauth_tokens, - oauth_bind_addr=args.oauth_bind_address, - oauth_port=args.oauth_port, - oauth_redirect_host=args.oauth_redirect_host, - ) - service_wrapper = GapiPeopleServiceWrapper( - service_provider=service_provider, - dry_mode=args.dry, - ) - - service = People( - email=args.email, - service_wrapper=service_wrapper, - # batch_size=args.batch_size, - batch_size=1, - storage=storage, - dry_mode=args.dry, - ) - - service.backup() - exit(1) + storage = FileStorage(os.path.join(args.workdir, args.email, args.service)) + storage_oauth_tokens = FileStorage(os.path.join(args.workdir, "oauth-tokens")) + service_provider_args = { + "credentials_file_path": args.credentials_filepath, + "service_account_email": args.service_account_email, + "service_account_file_path": args.service_account_key_filepath, + "storage": storage_oauth_tokens, + "oauth_bind_addr": args.oauth_bind_address, + "oauth_port": args.oauth_port, + "oauth_redirect_host": args.oauth_redirect_host, + } + + if args.service == "peoples": + service_provider = PeopleServiceProvider(**service_provider_args) + service_wrapper = GapiPeopleServiceWrapper( + service_provider=service_provider, + dry_mode=args.dry, + ) - if args.service == "gmail": - storage = FileStorage(args.workdir + "/" + args.email + "/gmail") - storage_oauth_tokens = FileStorage(args.workdir + "/oauth-tokens") - service_provider = GmailServiceProvider( - credentials_file_path=args.credentials_filepath, - service_account_email=args.service_account_email, - service_account_file_path=args.service_account_key_filepath, - storage=storage_oauth_tokens, - oauth_bind_addr=args.oauth_bind_address, - oauth_port=args.oauth_port, - oauth_redirect_host=args.oauth_redirect_host, + service = People( + email=args.email, + service_wrapper=service_wrapper, + # batch_size=args.batch_size, + batch_size=1, + storage=storage, + dry_mode=args.dry, ) + if args.command == "access-init": + service_wrapper.get_peoples(args.email) + elif args.command == "access-check": + try: + with service_provider.get_service(args.email, False) as s: + service_wrapper.get_peoples(args.email) + except AccessNotInitializedError: + exit(1) + elif args.command == "backup": + if service.backup(): + exit(0) + else: + exit(1) + else: + exit(1) + elif args.service == "gmail": + service_provider = GmailServiceProvider(**service_provider_args) service_wrapper = GapiGmailServiceWrapper( service_provider=service_provider, dry_mode=args.dry, ) - gmail = Gmail( + service = Gmail( email=args.email, service_wrapper=service_wrapper, batch_size=args.batch_size, @@ -268,7 +207,7 @@ def cli_startup(): except AccessNotInitializedError: exit(1) elif args.command == "backup": - if gmail.backup(quick_sync_days=args.quick_sync_days): + if service.backup(quick_sync_days=args.quick_sync_days): exit(0) else: exit(1) @@ -290,7 +229,7 @@ def cli_startup(): dt = parse_date(args.filter_date_to, args.timezone) item_filter.date_to(dt) logging.info(f"Filter options: date to {dt}") - if gmail.restore( + if service.restore( to_email=args.to_email, item_filter=item_filter, restore_deleted=args.restore_deleted, diff --git a/gwbackupy/cli/peoples_cli.py b/gwbackupy/cli/peoples_cli.py new file mode 100644 index 0000000..63c8d11 --- /dev/null +++ b/gwbackupy/cli/peoples_cli.py @@ -0,0 +1,56 @@ +from gwbackupy.helpers import parse_date + + +def add_cli_args_peoples(service_parser): + people_parser = service_parser.add_parser( + "peoples", help="Peoples (contacts) service commands" + ) + people_command_parser = people_parser.add_subparsers(dest="command") + people_oauth_init_parser = people_command_parser.add_parser( + "access-init", help="Access initialization e.g. OAuth authentication" + ) + people_oauth_init_parser.add_argument( + "--email", type=str, help="Email account", required=True + ) + people_oauth_check_parser = people_command_parser.add_parser( + "access-check", help="Check access e.g. OAuth tokens" + ) + people_oauth_check_parser.add_argument( + "--email", type=str, help="Email account", required=True + ) + people_backup_parser = people_command_parser.add_parser( + "backup", help="Backup people" + ) + people_backup_parser.add_argument( + "--email", type=str, help="Email account", required=True + ) + people_backup_parser.add_argument( + "--start-date", + type=parse_date, + help="Start date (inclusive)", + required=False, + ) + people_backup_parser.add_argument( + "--end-date", + type=parse_date, + help="End date (exclusive)", + required=False, + ) + people_restore_parser = people_command_parser.add_parser( + "restore", help="Restore people" + ) + people_restore_parser.add_argument( + "--email", type=str, help="Email account", required=True + ) + people_restore_parser.add_argument( + "--restore-deleted", + help="Restore deleted emails", + default=False, + action="store_true", + ) + people_restore_parser.add_argument( + "--restore-missing", + help="Restore missing emails", + default=False, + action="store_true", + ) diff --git a/setup.py b/setup.py index 778c44c..9f789bd 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ "gwbackupy.storage", "gwbackupy.filters", "gwbackupy.providers", + "gwbackupy.cli", ], url="https://github.com/smartondev/gwbackupy", license='BSD 3-Clause "New" or "Revised" License', From b9f4570aebe103bd30f0b0b2541d95bee59dadb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Somogyi?= Date: Sat, 18 Feb 2023 04:17:53 +0100 Subject: [PATCH 05/12] etag --- gwbackupy/people.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gwbackupy/people.py b/gwbackupy/people.py index 788df0c..fd1b20b 100644 --- a/gwbackupy/people.py +++ b/gwbackupy/people.py @@ -108,6 +108,8 @@ def __backup_item( write_meta = True # if any failure then write it force # ... + etag = people.get("etag", None) + print(etag) if write_meta: link = self.storage.new_link( From 3c656bbd03401e40d63eaecd149fa74c7fe02939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Somogyi?= Date: Sat, 18 Feb 2023 04:53:36 +0100 Subject: [PATCH 06/12] enh: peoples --- gwbackupy/people.py | 40 ++++++++++++++++--- .../providers/gapi_people_service_wrapper.py | 3 -- .../people_service_wrapper_interface.py | 3 -- gwbackupy/storage/storage_interface.py | 3 ++ 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/gwbackupy/people.py b/gwbackupy/people.py index fd1b20b..cd12994 100644 --- a/gwbackupy/people.py +++ b/gwbackupy/people.py @@ -5,6 +5,8 @@ import logging import threading +import requests + from gwbackupy import global_properties from gwbackupy.process_helpers import await_all_futures from gwbackupy.providers.people_service_wrapper_interface import ( @@ -109,14 +111,40 @@ def __backup_item( # ... etag = people.get("etag", None) - print(etag) + if not is_new: + etag_currently = latest_meta_link.get_property( + LinkInterface.property_etag + ) + if etag_currently is not None and etag_currently == etag: + write_meta = False + logging.debug(f"{people_id} is not changed, skip put") if write_meta: - link = self.storage.new_link( - object_id=people_id, - extension="json", - created_timestamp=0.0, - ).set_properties({LinkInterface.property_metadata: True}) + photos = people.get("photos", []) + for photo in photos: + photo_url = photo.get("url", None) + if photo_url is None or photo.get("default", True) is True: + # not found url or default photo + continue + logging.debug(f"{people_id} downloading photo: {photo_url}") + r = requests.get(photo_url, stream=True) + if r.status_code != 200: + logging.error( + f"{people_id} photo download failed ({photo_url})" + ) + continue + logging.debug( + f"{people_id} photo download success ({len(r.raw)} bytes)" + ) + link = ( + self.storage.new_link( + object_id=people_id, + extension="json", + created_timestamp=0.0, + ) + .set_properties({LinkInterface.property_metadata: True}) + .set_properties({LinkInterface.property_etag: etag}) + ) success = self.__storage_put(link, data=json.dumps(people)) if success: logging.info(f"{people_id} meta data is saved") diff --git a/gwbackupy/providers/gapi_people_service_wrapper.py b/gwbackupy/providers/gapi_people_service_wrapper.py index a599442..bc6deae 100644 --- a/gwbackupy/providers/gapi_people_service_wrapper.py +++ b/gwbackupy/providers/gapi_people_service_wrapper.py @@ -61,6 +61,3 @@ def get_peoples(self, email: str) -> dict[str, [dict[str, any]]]: def get_people(self, email: str, contact_id: str) -> [dict[str, any]]: pass - - def get_people_main_photo(self, email: str, contact_id: str) -> bytes: - pass diff --git a/gwbackupy/providers/people_service_wrapper_interface.py b/gwbackupy/providers/people_service_wrapper_interface.py index 2dd1b56..04566f7 100644 --- a/gwbackupy/providers/people_service_wrapper_interface.py +++ b/gwbackupy/providers/people_service_wrapper_interface.py @@ -7,6 +7,3 @@ def get_peoples(self, email: str) -> dict[str, [dict[str, any]]]: def get_people(self, email: str, contact_id: str) -> [dict[str, any]]: pass - - def get_people_main_photo(self, email: str, contact_id: str) -> bytes: - pass diff --git a/gwbackupy/storage/storage_interface.py b/gwbackupy/storage/storage_interface.py index 15054c4..16a31cb 100644 --- a/gwbackupy/storage/storage_interface.py +++ b/gwbackupy/storage/storage_interface.py @@ -17,6 +17,9 @@ class LinkInterface: property_object = "object" property_mutation = "mutation" property_content_hash = "ch" + """Content hash. Used to check if the content is changed. Calculated by content.""" + property_etag = "etag" + """ETag. Used to check if the content is changed. Calculated by API.""" id_special_prefix = "--gwbackupy-" def id(self) -> str: From ac8a886c01cad4e662a482bf5870429f0962b5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Somogyi?= Date: Sat, 18 Feb 2023 07:38:29 +0100 Subject: [PATCH 07/12] . --- gwbackupy/people.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gwbackupy/people.py b/gwbackupy/people.py index cd12994..e588148 100644 --- a/gwbackupy/people.py +++ b/gwbackupy/people.py @@ -119,7 +119,8 @@ def __backup_item( write_meta = False logging.debug(f"{people_id} is not changed, skip put") - if write_meta: + logging.debug(f"{people_id} processing photos... {people}") + if write_meta or True: photos = people.get("photos", []) for photo in photos: photo_url = photo.get("url", None) From 8af289a98100d78f94487d3255cb8a780d2d6830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Somogyi?= Date: Sun, 19 Feb 2023 15:52:26 +0100 Subject: [PATCH 08/12] contacts --- gwbackupy/helpers.py | 7 +++++ gwbackupy/people.py | 55 +++++++++++++++++++++++---------- gwbackupy/tests/test_helpers.py | 10 ++++++ 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/gwbackupy/helpers.py b/gwbackupy/helpers.py index b4cbd74..525f357 100644 --- a/gwbackupy/helpers.py +++ b/gwbackupy/helpers.py @@ -8,6 +8,7 @@ import logging from json import JSONDecodeError from typing import IO +import hashlib import tzlocal from googleapiclient.errors import HttpError @@ -82,3 +83,9 @@ def is_rate_limit_exceeded(e) -> bool: def random_string(length: int = 8) -> str: return "".join(random.choice(string.ascii_lowercase) for _ in range(length)) + + +def md5hex(data: bytes | str) -> str: + if isinstance(data, str): + data = data.encode("utf-8") + return hashlib.md5(data).hexdigest().lower() diff --git a/gwbackupy/people.py b/gwbackupy/people.py index e588148..07e7a92 100644 --- a/gwbackupy/people.py +++ b/gwbackupy/people.py @@ -6,8 +6,10 @@ import threading import requests +import re from gwbackupy import global_properties +from gwbackupy.helpers import md5hex from gwbackupy.process_helpers import await_all_futures from gwbackupy.providers.people_service_wrapper_interface import ( PeopleServiceWrapperInterface, @@ -121,22 +123,7 @@ def __backup_item( logging.debug(f"{people_id} processing photos... {people}") if write_meta or True: - photos = people.get("photos", []) - for photo in photos: - photo_url = photo.get("url", None) - if photo_url is None or photo.get("default", True) is True: - # not found url or default photo - continue - logging.debug(f"{people_id} downloading photo: {photo_url}") - r = requests.get(photo_url, stream=True) - if r.status_code != 200: - logging.error( - f"{people_id} photo download failed ({photo_url})" - ) - continue - logging.debug( - f"{people_id} photo download success ({len(r.raw)} bytes)" - ) + self.__backup_photos(people, people_id) link = ( self.storage.new_link( object_id=people_id, @@ -161,6 +148,42 @@ def __backup_item( return logging.exception(f"{people_id} {e}") + def __backup_photos(self, people, people_id): + # TODO precheck download files, and download only if not exists + photos = people.get("photos", []) + for photo in photos: + photo_url = photo.get("url", None) + if photo_url is None: + # not found url or default photo + continue + photo_url = re.sub(r"=s100$", "", photo_url) + logging.debug(f"{people_id} downloading photo: {photo_url}") + r = requests.get(photo_url, stream=True) + if r.status_code != 200: + raise Exception(f"photo download failed ({photo_url}") + for header in r.headers: + logging.log( + global_properties.log_finest, f"{header}: {r.headers[header]}" + ) + extension = r.headers.get("content-type", "unknown").split("/")[-1] + photo_bytes = b"" + for chunk in r.iter_content(chunk_size=1024): + photo_bytes += chunk + size = len(photo_bytes) + logging.debug(f"{people_id} photo download success ({size} bytes)") + link = ( + self.storage.new_link( + object_id=people_id, + extension=extension, + created_timestamp=0.0, + ) + .set_properties({LinkInterface.property_object: True}) + .set_properties({LinkInterface.property_etag: md5hex(photo_url)}) + ) + if self.__storage_put(link, data=photo_bytes): + logging.info(f"{people_id} photo is saved") + logging.debug(f"{people_id} saving photo: {link}") + def __storage_put(self, link: LinkInterface, data: Data) -> bool: if self.dry_mode: logging.info(f"DRY MODE storage put: {link}") diff --git a/gwbackupy/tests/test_helpers.py b/gwbackupy/tests/test_helpers.py index 1d44979..29bc9b2 100644 --- a/gwbackupy/tests/test_helpers.py +++ b/gwbackupy/tests/test_helpers.py @@ -13,6 +13,7 @@ parse_date, is_rate_limit_exceeded, random_string, + md5hex, ) @@ -94,3 +95,12 @@ def test_is_rate_limit_exceeded(): def test_random_string(): for i in range(32): assert len(random_string(i)) == i + + +def test_md5hex(): + assert md5hex(b"") == "d41d8cd98f00b204e9800998ecf8427e" + assert md5hex(b"abc") == "900150983cd24fb0d6963f7d28e17f72" + assert md5hex(b"message digest") == "f96b697d7cb7938d525a2f31aaf161d0" + assert md5hex("") == "d41d8cd98f00b204e9800998ecf8427e" + assert md5hex("abc") == "900150983cd24fb0d6963f7d28e17f72" + assert md5hex("message digest") == "f96b697d7cb7938d525a2f31aaf161d0" From 668597aeabfd3e61fcf64363724e58f69bed1a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Somogyi?= Date: Mon, 20 Feb 2023 03:51:07 +0100 Subject: [PATCH 09/12] contacts --- gwbackupy/people.py | 51 +++++++++++++------ .../providers/gapi_people_service_wrapper.py | 7 +-- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/gwbackupy/people.py b/gwbackupy/people.py index 07e7a92..4199abb 100644 --- a/gwbackupy/people.py +++ b/gwbackupy/people.py @@ -46,14 +46,17 @@ def backup(self): stored_data_all = self.storage.find() logging.info(f"Stored items: {len(stored_data_all)}") - stored_items: dict[str, dict[int, LinkInterface]] = stored_data_all.find( + stored_items: dict[str, dict[str, LinkInterface]] = stored_data_all.find( f=lambda l: not l.is_special_id() and (l.is_metadata() or l.is_object()), - g=lambda l: [l.id(), 0 if l.is_metadata() else 1], + g=lambda l: [ + l.id(), + "" if l.is_metadata() else l.get_property(LinkInterface.property_etag), + ], ) del stored_data_all for item_id in list(stored_items.keys()): - link_metadata = stored_items[item_id].get(0) + link_metadata = stored_items[item_id].get("") if link_metadata is None: logging.error(f"{item_id} metadata is not found in locally") del stored_items[item_id] @@ -97,14 +100,16 @@ def backup(self): return False def __backup_item( - self, people: dict[str, any], stored_items: dict[str, dict[int, LinkInterface]] + self, people: dict[str, any], stored_items: dict[str, dict[str, LinkInterface]] ): people_id = people.get("resourceName", "UNKNOWN") # for logging try: people_id = people["resourceName"] + links: dict[str, LinkInterface] = dict() latest_meta_link = None if people_id in stored_items: - latest_meta_link = stored_items[people_id][0] + latest_meta_link = stored_items[people_id][""] + links = stored_items[people_id] is_new = latest_meta_link is None if is_new: logging.debug(f"{people_id} is new") @@ -122,8 +127,8 @@ def __backup_item( logging.debug(f"{people_id} is not changed, skip put") logging.debug(f"{people_id} processing photos... {people}") - if write_meta or True: - self.__backup_photos(people, people_id) + if write_meta: + self.__backup_photos(people, people_id, links) link = ( self.storage.new_link( object_id=people_id, @@ -133,8 +138,7 @@ def __backup_item( .set_properties({LinkInterface.property_metadata: True}) .set_properties({LinkInterface.property_etag: etag}) ) - success = self.__storage_put(link, data=json.dumps(people)) - if success: + if self.__storage_put(link, data=json.dumps(people)): logging.info(f"{people_id} meta data is saved") else: raise Exception("Meta data put failed") @@ -148,15 +152,20 @@ def __backup_item( return logging.exception(f"{people_id} {e}") - def __backup_photos(self, people, people_id): - # TODO precheck download files, and download only if not exists - photos = people.get("photos", []) - for photo in photos: + def __backup_photos( + self, people: dict[str, any], people_id: str, links: dict[str, LinkInterface] + ): + for photo in people.get("photos", []): photo_url = photo.get("url", None) if photo_url is None: # not found url or default photo continue photo_url = re.sub(r"=s100$", "", photo_url) + photo_url_md5 = md5hex(photo_url) + if photo_url_md5 in links: + # already exists + links.pop(photo_url_md5) + continue logging.debug(f"{people_id} downloading photo: {photo_url}") r = requests.get(photo_url, stream=True) if r.status_code != 200: @@ -181,8 +190,20 @@ def __backup_photos(self, people, people_id): .set_properties({LinkInterface.property_etag: md5hex(photo_url)}) ) if self.__storage_put(link, data=photo_bytes): - logging.info(f"{people_id} photo is saved") - logging.debug(f"{people_id} saving photo: {link}") + logging.info(f"{people_id} photo is saved ({photo_url})") + else: + raise Exception(f"Photo put failed ({link})") + # delete old photos + for photo_url_md5 in links: + if photo_url_md5 == "": + continue + logging.info(f"{people_id} deleting photo link: {links[photo_url_md5]}") + if self.storage.remove(links[photo_url_md5]): + logging.debug( + f"{people_id} photo link is deleted ({links[photo_url_md5]})" + ) + else: + raise Exception(f"Photo link delete failed ({links[photo_url_md5]})") def __storage_put(self, link: LinkInterface, data: Data) -> bool: if self.dry_mode: diff --git a/gwbackupy/providers/gapi_people_service_wrapper.py b/gwbackupy/providers/gapi_people_service_wrapper.py index bc6deae..0e34d65 100644 --- a/gwbackupy/providers/gapi_people_service_wrapper.py +++ b/gwbackupy/providers/gapi_people_service_wrapper.py @@ -2,6 +2,7 @@ import logging +from gwbackupy import global_properties from gwbackupy.providers.people_service_provider import PeopleServiceProvider from gwbackupy.providers.people_service_wrapper_interface import ( PeopleServiceWrapperInterface, @@ -36,7 +37,7 @@ def get_peoples(self, email: str) -> dict[str, [dict[str, any]]]: .connections() .list( resourceName="people/me", - pageSize=50, + pageSize=2000, pageToken=next_page_token, personFields="addresses,ageRange,biographies,birthdays,braggingRights,coverPhotos," "events,genders,imClients,interests,locales,memberships,metadata,names,nicknames," @@ -53,10 +54,10 @@ def get_peoples(self, email: str) -> dict[str, [dict[str, any]]]: ) for item in data.get("connections", []): items[item.get("resourceName")] = item - page += 1 if next_page_token is None: break - print(items) + page += 1 + logging.log(global_properties.log_finest, f"Items: {items}") return items def get_people(self, email: str, contact_id: str) -> [dict[str, any]]: From 53daf8f7a2f5fb21c30540d2ee29297df89db22b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Somogyi?= Date: Mon, 20 Feb 2023 04:12:39 +0100 Subject: [PATCH 10/12] contacts --- gwbackupy/people.py | 23 +++++++------------ .../providers/gapi_people_service_wrapper.py | 22 ++++++++++++++++-- .../people_service_wrapper_interface.py | 9 +++++++- requirements.txt | 6 +++-- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/gwbackupy/people.py b/gwbackupy/people.py index 4199abb..907beda 100644 --- a/gwbackupy/people.py +++ b/gwbackupy/people.py @@ -166,20 +166,13 @@ def __backup_photos( # already exists links.pop(photo_url_md5) continue - logging.debug(f"{people_id} downloading photo: {photo_url}") - r = requests.get(photo_url, stream=True) - if r.status_code != 200: - raise Exception(f"photo download failed ({photo_url}") - for header in r.headers: - logging.log( - global_properties.log_finest, f"{header}: {r.headers[header]}" - ) - extension = r.headers.get("content-type", "unknown").split("/")[-1] - photo_bytes = b"" - for chunk in r.iter_content(chunk_size=1024): - photo_bytes += chunk - size = len(photo_bytes) - logging.debug(f"{people_id} photo download success ({size} bytes)") + descriptor = self.__service_wrapper.get_photo( + email=self.email, uri=photo_url, people_id=people_id + ) + extension = descriptor.mime_type.split("/")[-1] + logging.debug( + f"{people_id} photo download success ({len(descriptor.data)} bytes / {descriptor.mime_type})" + ) link = ( self.storage.new_link( object_id=people_id, @@ -189,7 +182,7 @@ def __backup_photos( .set_properties({LinkInterface.property_object: True}) .set_properties({LinkInterface.property_etag: md5hex(photo_url)}) ) - if self.__storage_put(link, data=photo_bytes): + if self.__storage_put(link, data=descriptor.data): logging.info(f"{people_id} photo is saved ({photo_url})") else: raise Exception(f"Photo put failed ({link})") diff --git a/gwbackupy/providers/gapi_people_service_wrapper.py b/gwbackupy/providers/gapi_people_service_wrapper.py index 0e34d65..295cdfb 100644 --- a/gwbackupy/providers/gapi_people_service_wrapper.py +++ b/gwbackupy/providers/gapi_people_service_wrapper.py @@ -2,10 +2,13 @@ import logging +import requests + from gwbackupy import global_properties from gwbackupy.providers.people_service_provider import PeopleServiceProvider from gwbackupy.providers.people_service_wrapper_interface import ( PeopleServiceWrapperInterface, + PhotoDescriptor, ) @@ -60,5 +63,20 @@ def get_peoples(self, email: str) -> dict[str, [dict[str, any]]]: logging.log(global_properties.log_finest, f"Items: {items}") return items - def get_people(self, email: str, contact_id: str) -> [dict[str, any]]: - pass + def get_photo(self, email: str, people_id: str, uri: str) -> PhotoDescriptor: + logging.debug(f"{people_id} downloading photo: {uri}") + r = requests.get(uri, stream=True) + if r.status_code != 200: + raise Exception( + f"photo download failed, status code: {r.status_code} ({uri})" + ) + for header in r.headers: + logging.log(global_properties.log_finest, f"{header}: {r.headers[header]}") + photo_bytes = b"" + for chunk in r.iter_content(chunk_size=1024): + photo_bytes += chunk + return PhotoDescriptor( + uri=uri, + data=photo_bytes, + mime_type=r.headers.get("content-type", "image/unknown"), + ) diff --git a/gwbackupy/providers/people_service_wrapper_interface.py b/gwbackupy/providers/people_service_wrapper_interface.py index 04566f7..c3b9279 100644 --- a/gwbackupy/providers/people_service_wrapper_interface.py +++ b/gwbackupy/providers/people_service_wrapper_interface.py @@ -5,5 +5,12 @@ class PeopleServiceWrapperInterface: def get_peoples(self, email: str) -> dict[str, [dict[str, any]]]: pass - def get_people(self, email: str, contact_id: str) -> [dict[str, any]]: + def get_photo(self, email: str, people_id: str, uri: str) -> PhotoDescriptor: pass + + +class PhotoDescriptor: + def __init__(self, uri: str, data: bytes, mime_type: str): + self.uri = uri + self.data = data + self.mime_type = mime_type diff --git a/requirements.txt b/requirements.txt index be2ec7b..3e4a8d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,9 @@ -google-api-python-client~=2.71 +google-api-python-client==2.78.0 oauth2client~=4.1 pyopenssl~=23.0 tzlocal~=4.2 pytz~=2022.7 google-auth-httplib2~=0.1.0 -google-auth-oauthlib~=0.8.0 \ No newline at end of file +google-auth-oauthlib==1.0.0 +setuptools==67.3.2 +requests~=2.28.2 \ No newline at end of file From d7f23bad8aaa26da375322409bf03e3b6ffe64d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Somogyi?= Date: Mon, 20 Feb 2023 04:49:17 +0100 Subject: [PATCH 11/12] contacts --- gwbackupy/people.py | 18 +++++++++++------- .../providers/gapi_people_service_wrapper.py | 1 - gwbackupy/storage/file_storage.py | 10 +++++++--- gwbackupy/storage/storage_interface.py | 1 + 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/gwbackupy/people.py b/gwbackupy/people.py index 907beda..9c303b3 100644 --- a/gwbackupy/people.py +++ b/gwbackupy/people.py @@ -126,14 +126,15 @@ def __backup_item( write_meta = False logging.debug(f"{people_id} is not changed, skip put") - logging.debug(f"{people_id} processing photos... {people}") if write_meta: - self.__backup_photos(people, people_id, links) + logging.info(f"{people_id} is changed") + folders = ["people", people_id.split("/")[1][0:3]] + self.__backup_photos(people, people_id, links, folders) link = ( self.storage.new_link( object_id=people_id, extension="json", - created_timestamp=0.0, + folders=folders, ) .set_properties({LinkInterface.property_metadata: True}) .set_properties({LinkInterface.property_etag: etag}) @@ -142,8 +143,6 @@ def __backup_item( logging.info(f"{people_id} meta data is saved") else: raise Exception("Meta data put failed") - else: - logging.debug(f"{people_id} meta data is not changed, skip put") except Exception as e: with self.__lock: @@ -153,8 +152,13 @@ def __backup_item( logging.exception(f"{people_id} {e}") def __backup_photos( - self, people: dict[str, any], people_id: str, links: dict[str, LinkInterface] + self, + people: dict[str, any], + people_id: str, + links: dict[str, LinkInterface], + folders: [str], ): + logging.debug(f"{people_id} processing photos...") for photo in people.get("photos", []): photo_url = photo.get("url", None) if photo_url is None: @@ -177,7 +181,7 @@ def __backup_photos( self.storage.new_link( object_id=people_id, extension=extension, - created_timestamp=0.0, + folders=folders, ) .set_properties({LinkInterface.property_object: True}) .set_properties({LinkInterface.property_etag: md5hex(photo_url)}) diff --git a/gwbackupy/providers/gapi_people_service_wrapper.py b/gwbackupy/providers/gapi_people_service_wrapper.py index 295cdfb..dbf901d 100644 --- a/gwbackupy/providers/gapi_people_service_wrapper.py +++ b/gwbackupy/providers/gapi_people_service_wrapper.py @@ -49,7 +49,6 @@ def get_peoples(self, email: str) -> dict[str, [dict[str, any]]]: ) .execute() ) - print(data) next_page_token = data.get("nextPageToken", None) page_message_count = len(data.get("connections", [])) logging.debug( diff --git a/gwbackupy/storage/file_storage.py b/gwbackupy/storage/file_storage.py index 6846e65..c5a80dd 100644 --- a/gwbackupy/storage/file_storage.py +++ b/gwbackupy/storage/file_storage.py @@ -198,16 +198,20 @@ def new_link( object_id: str, extension: str, created_timestamp: int | float | None = None, + folders: list[str] | None = None, ) -> FileLink: link = FileLink() path = self.root - if created_timestamp is not None: - sub_paths = ( + if folders is not None: + folders.insert(0, path) + path = os.path.join(*folders) + elif created_timestamp is not None: + sub_paths: [str] = ( datetime.fromtimestamp(created_timestamp, tz=timezone.utc) .strftime("%Y-%m-%d") .split("-", 1) ) - path += f"/{sub_paths[0]}/{sub_paths[1]}" + path = os.path.join(path, sub_paths[0], sub_paths[1]) link.fill( { "path": path, diff --git a/gwbackupy/storage/storage_interface.py b/gwbackupy/storage/storage_interface.py index 16a31cb..03cd18c 100644 --- a/gwbackupy/storage/storage_interface.py +++ b/gwbackupy/storage/storage_interface.py @@ -143,6 +143,7 @@ def new_link( object_id: str, extension: str, created_timestamp: int | float | None = None, + folders: list[str] | None = None, ) -> LinkInterface: raise NotImplementedError("StorageInterface#new_link") From 8b7adc3182708d8a356298cdd6150d9db7e9fb82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Somogyi?= Date: Mon, 20 Feb 2023 04:56:44 +0100 Subject: [PATCH 12/12] contacts --- gwbackupy/people.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/gwbackupy/people.py b/gwbackupy/people.py index 9c303b3..d0c4719 100644 --- a/gwbackupy/people.py +++ b/gwbackupy/people.py @@ -5,7 +5,6 @@ import logging import threading -import requests import re from gwbackupy import global_properties @@ -194,10 +193,9 @@ def __backup_photos( for photo_url_md5 in links: if photo_url_md5 == "": continue - logging.info(f"{people_id} deleting photo link: {links[photo_url_md5]}") if self.storage.remove(links[photo_url_md5]): - logging.debug( - f"{people_id} photo link is deleted ({links[photo_url_md5]})" + logging.info( + f"{people_id} old photo is deleted ({links[photo_url_md5]})" ) else: raise Exception(f"Photo link delete failed ({links[photo_url_md5]})")