MExpert

Google Takeout Import Script

Apr 23rd, 2025
40
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 24.72 KB | Source Code | 0 0
  1. import os
  2. import json
  3. import time
  4. import base64
  5. import email
  6. import vobject
  7. import pickle
  8. import mimetypes
  9. import ics
  10. from pathlib import Path
  11. from datetime import datetime, timedelta
  12. from googleapiclient.discovery import build
  13. from googleapiclient.http import MediaFileUpload
  14. from google_auth_oauthlib.flow import InstalledAppFlow
  15. from google.auth.transport.requests import Request
  16. from tqdm import tqdm
  17.  
  18. class GoogleTakeoutImporter:
  19.     """A class to import Google Takeout data back into Google services."""
  20.    
  21.     # Define the scopes needed for different services
  22.     SCOPES = [
  23.         'https://www.googleapis.com/auth/drive',
  24.         'https://www.googleapis.com/auth/gmail.modify',
  25.         'https://www.googleapis.com/auth/calendar',
  26.         'https://www.googleapis.com/auth/contacts'
  27.     ]
  28.    
  29.     def __init__(self, client_secret_file, takeout_dir):
  30.         """Initialize the importer with client secret and takeout directory."""
  31.         self.client_secret_file = client_secret_file
  32.         self.takeout_dir = Path(takeout_dir)
  33.         self.credentials = self._get_credentials()
  34.        
  35.         # Initialize services as None, to be created on demand
  36.         self.drive_service = None
  37.         self.gmail_service = None
  38.         self.calendar_service = None
  39.         self.contacts_service = None
  40.    
  41.     def _get_credentials(self):
  42.         """Get and refresh OAuth 2.0 credentials."""
  43.         creds = None
  44.         token_file = 'token.pickle'
  45.        
  46.         # Check if we have saved credentials
  47.         if os.path.exists(token_file):
  48.             with open(token_file, 'rb') as token:
  49.                 creds = pickle.load(token)
  50.        
  51.         # If credentials don't exist or are invalid, get new ones
  52.         if not creds or not creds.valid:
  53.             if creds and creds.expired and creds.refresh_token:
  54.                 creds.refresh(Request())
  55.             else:
  56.                 flow = InstalledAppFlow.from_client_secrets_file(
  57.                     self.client_secret_file, self.SCOPES)
  58.                 creds = flow.run_local_server(port=0)
  59.            
  60.             # Save credentials for next run
  61.             with open(token_file, 'wb') as token:
  62.                 pickle.dump(creds, token)
  63.        
  64.         return creds
  65.    
  66.     def _get_drive_service(self):
  67.         """Get or create the Drive API service."""
  68.         if not self.drive_service:
  69.             self.drive_service = build('drive', 'v3', credentials=self.credentials)
  70.         return self.drive_service
  71.    
  72.     def _get_gmail_service(self):
  73.         """Get or create the Gmail API service."""
  74.         if not self.gmail_service:
  75.             self.gmail_service = build('gmail', 'v1', credentials=self.credentials)
  76.         return self.gmail_service
  77.    
  78.     def _get_calendar_service(self):
  79.         """Get or create the Calendar API service."""
  80.         if not self.calendar_service:
  81.             self.calendar_service = build('calendar', 'v3', credentials=self.credentials)
  82.         return self.calendar_service
  83.    
  84.     def _get_contacts_service(self):
  85.         """Get or create the People (Contacts) API service."""
  86.         if not self.contacts_service:
  87.             self.contacts_service = build('people', 'v1', credentials=self.credentials)
  88.         return self.contacts_service
  89.    
  90.     def _exponential_backoff(self, attempt, max_attempts=5, initial_delay=1):
  91.         """Implement exponential backoff for API rate limits."""
  92.         if attempt >= max_attempts:
  93.             raise Exception(f"Maximum retry attempts ({max_attempts}) exceeded")
  94.        
  95.         delay = initial_delay * (2 ** attempt)
  96.         time.sleep(delay)
  97.    
  98.     def import_drive_files(self, folder_path=None):
  99.         """Import files from the Takeout Drive folder to Google Drive."""
  100.         if folder_path:
  101.             drive_dir = Path(folder_path)
  102.         else:
  103.             drive_dir = self.takeout_dir / "Drive"
  104.        
  105.         if not drive_dir.exists():
  106.             print(f"Drive folder not found at {drive_dir}")
  107.             return
  108.        
  109.         service = self._get_drive_service()
  110.        
  111.         # Create a mapping of folder paths to folder IDs for organizing files
  112.         folder_mapping = {'root': 'root'}
  113.        
  114.         # Function to recursively upload files and folders
  115.         def upload_folder(folder_path, parent_id='root'):
  116.             print(f"Processing folder: {folder_path}")
  117.            
  118.             # First, create all the folders
  119.             for item in folder_path.iterdir():
  120.                 if item.is_dir():
  121.                     print(f"Creating folder: {item.name}")
  122.                    
  123.                     # Create the folder in Drive
  124.                     folder_metadata = {
  125.                         'name': item.name,
  126.                         'mimeType': 'application/vnd.google-apps.folder',
  127.                         'parents': [parent_id]
  128.                     }
  129.                    
  130.                     for attempt in range(5):
  131.                         try:
  132.                             folder = service.files().create(
  133.                                 body=folder_metadata,
  134.                                 fields='id'
  135.                             ).execute()
  136.                            
  137.                             folder_id = folder.get('id')
  138.                             folder_mapping[str(item)] = folder_id
  139.                            
  140.                             # Recursively upload the folder's contents
  141.                             upload_folder(item, folder_id)
  142.                             break
  143.                         except Exception as e:
  144.                             print(f"Error creating folder {item.name}, attempt {attempt+1}: {e}")
  145.                             self._exponential_backoff(attempt)
  146.            
  147.             # Then upload all files in the current folder
  148.             for item in folder_path.iterdir():
  149.                 if item.is_file():
  150.                     print(f"Uploading file: {item.name}")
  151.                     mimetype = mimetypes.guess_type(item)[0]
  152.                     if not mimetype:
  153.                         mimetype = 'application/octet-stream'
  154.                    
  155.                     file_metadata = {
  156.                         'name': item.name,
  157.                         'parents': [parent_id]
  158.                     }
  159.                    
  160.                     media = MediaFileUpload(
  161.                         str(item),
  162.                         mimetype=mimetype,
  163.                         resumable=True
  164.                     )
  165.                    
  166.                     for attempt in range(5):
  167.                         try:
  168.                             service.files().create(
  169.                                 body=file_metadata,
  170.                                 media_body=media,
  171.                                 fields='id'
  172.                             ).execute()
  173.                             break
  174.                         except Exception as e:
  175.                             print(f"Error uploading file {item.name}, attempt {attempt+1}: {e}")
  176.                             self._exponential_backoff(attempt)
  177.        
  178.         # Start the upload process
  179.         upload_folder(drive_dir)
  180.         print("Drive file import completed!")
  181.    
  182.     def import_gmail_messages(self, mbox_path=None):
  183.         """Import emails from the Takeout Mail folder to Gmail."""
  184.         if mbox_path:
  185.             mail_path = Path(mbox_path)
  186.         else:
  187.             mail_path = self.takeout_dir / "Mail"
  188.        
  189.         if not mail_path.exists():
  190.             print(f"Mail folder not found at {mail_path}")
  191.             return
  192.        
  193.         service = self._get_gmail_service()
  194.        
  195.         # Find all .mbox files in the Mail directory
  196.         mbox_files = list(mail_path.glob("**/*.mbox"))
  197.        
  198.         if not mbox_files:
  199.             print("No .mbox files found")
  200.             return
  201.        
  202.         for mbox_file in mbox_files:
  203.             print(f"Processing mail file: {mbox_file}")
  204.            
  205.             # Parse the .mbox file (simplified approach)
  206.             with open(mbox_file, 'r', encoding='utf-8', errors='ignore') as f:
  207.                 content = f.read()
  208.            
  209.             # Split by 'From ' header which typically separates messages in mbox format
  210.             raw_messages = content.split('\nFrom ')
  211.             if raw_messages[0].startswith('From '):
  212.                 raw_messages[0] = raw_messages[0][5:]  # Remove the leading 'From ' from the first message
  213.             else:
  214.                 raw_messages = raw_messages[1:]  # Skip the first split if it doesn't start with 'From '
  215.            
  216.             print(f"Found {len(raw_messages)} messages")
  217.            
  218.             for idx, raw_message in enumerate(raw_messages):
  219.                 if not raw_message.strip():
  220.                     continue
  221.                
  222.                 # Add back the 'From ' prefix except for the first message
  223.                 if idx > 0:
  224.                     raw_message = 'From ' + raw_message
  225.                
  226.                 try:
  227.                     # Parse the message
  228.                     msg = email.message_from_string(raw_message)
  229.                    
  230.                     # Convert to Gmail API format
  231.                     encoded_message = base64.urlsafe_b64encode(raw_message.encode()).decode()
  232.                    
  233.                     body = {
  234.                         'raw': encoded_message
  235.                     }
  236.                    
  237.                     # Import to Gmail
  238.                     for attempt in range(5):
  239.                         try:
  240.                             service.users().messages().import_(
  241.                                 userId='me',
  242.                                 body=body
  243.                             ).execute()
  244.                            
  245.                             if (idx + 1) % 10 == 0:
  246.                                 print(f"Imported {idx + 1} messages...")
  247.                            
  248.                             break
  249.                         except Exception as e:
  250.                             print(f"Error importing message {idx}, attempt {attempt+1}: {e}")
  251.                             self._exponential_backoff(attempt)
  252.                
  253.                 except Exception as e:
  254.                     print(f"Error processing message {idx}: {e}")
  255.        
  256.         print("Gmail message import completed!")
  257.    
  258.     def import_calendar_events(self, calendar_dir=None):
  259.         """Import calendar events from the Takeout Calendar folder."""
  260.         if calendar_dir:
  261.             calendar_path = Path(calendar_dir)
  262.         else:
  263.             calendar_path = self.takeout_dir / "Calendar"
  264.        
  265.         if not calendar_path.exists():
  266.             print(f"Calendar folder not found at {calendar_path}")
  267.             return
  268.        
  269.         service = self._get_calendar_service()
  270.        
  271.         # Find all .ics files
  272.         ics_files = list(calendar_path.glob("**/*.ics"))
  273.        
  274.         if not ics_files:
  275.             print("No .ics files found")
  276.             return
  277.        
  278.         print(f"Found {len(ics_files)} calendar files")
  279.        
  280.         # Get list of calendars to choose where to import
  281.         calendars = {}
  282.         try:
  283.             calendar_list = service.calendarList().list().execute()
  284.             for calendar_item in calendar_list.get('items', []):
  285.                 calendar_id = calendar_item['id']
  286.                 calendar_summary = calendar_item['summary']
  287.                 calendars[calendar_summary] = calendar_id
  288.                 print(f"- {calendar_summary}")
  289.         except Exception as e:
  290.             print(f"Error fetching calendars: {e}")
  291.             return
  292.        
  293.         # Ask user which calendar to use
  294.         print("\nPlease choose a calendar to import events into:")
  295.         for i, calendar_name in enumerate(calendars.keys(), 1):
  296.             print(f"{i}. {calendar_name}")
  297.        
  298.         choice = input("\nEnter calendar number (or press Enter for primary): ")
  299.        
  300.         if choice.strip():
  301.             try:
  302.                 calendar_name = list(calendars.keys())[int(choice) - 1]
  303.                 calendar_id = calendars[calendar_name]
  304.                 print(f"Selected calendar: {calendar_name}")
  305.             except (IndexError, ValueError):
  306.                 print("Invalid choice, using primary calendar")
  307.                 calendar_id = 'primary'
  308.         else:
  309.             print("Using primary calendar")
  310.             calendar_id = 'primary'
  311.        
  312.         # Process each calendar file
  313.         total_events = 0
  314.         imported_events = 0
  315.        
  316.         for ics_file in ics_files:
  317.             print(f"Processing calendar file: {ics_file}")
  318.            
  319.             try:
  320.                 with open(ics_file, 'r', encoding='utf-8', errors='ignore') as f:
  321.                     ics_content = f.read()
  322.                
  323.                 # Parse the calendar file
  324.                 calendar = ics.Calendar(ics_content)
  325.                
  326.                 # Process each event
  327.                 for event in calendar.events:
  328.                     total_events += 1
  329.                    
  330.                     try:
  331.                         # Convert ICS event to Google Calendar format
  332.                         google_event = self._convert_ics_to_google_event(event)
  333.                        
  334.                         # Import to Calendar
  335.                         for attempt in range(5):
  336.                             try:
  337.                                 service.events().insert(
  338.                                     calendarId=calendar_id,
  339.                                     body=google_event
  340.                                 ).execute()
  341.                                
  342.                                 imported_events += 1
  343.                                 if imported_events % 10 == 0:
  344.                                     print(f"Imported {imported_events} events...")
  345.                                
  346.                                 break
  347.                             except Exception as e:
  348.                                 print(f"Error importing event, attempt {attempt+1}: {e}")
  349.                                 self._exponential_backoff(attempt)
  350.                    
  351.                     except Exception as e:
  352.                         print(f"Error processing event: {e}")
  353.            
  354.             except Exception as e:
  355.                 print(f"Error reading calendar file {ics_file}: {e}")
  356.        
  357.         print(f"Calendar import completed! Imported {imported_events} out of {total_events} events.")
  358.    
  359.     def _convert_ics_to_google_event(self, ics_event):
  360.         """Convert an ICS event to Google Calendar API format."""
  361.         event = {}
  362.        
  363.         # Basic event details
  364.         if ics_event.name:
  365.             event['summary'] = ics_event.name
  366.        
  367.         if ics_event.description:
  368.             event['description'] = ics_event.description
  369.        
  370.         if ics_event.location:
  371.             event['location'] = ics_event.location
  372.        
  373.         # Handle start and end times
  374.         if ics_event.begin:
  375.             start_time = ics_event.begin.datetime
  376.            
  377.             # Check if it's an all-day event
  378.             if start_time.hour == 0 and start_time.minute == 0 and start_time.second == 0:
  379.                 # All day events need date only
  380.                 event['start'] = {'date': start_time.strftime('%Y-%m-%d')}
  381.                
  382.                 # For all-day events, end date is exclusive in Google Calendar
  383.                 end_time = ics_event.end.datetime if ics_event.end else (start_time + timedelta(days=1))
  384.                 event['end'] = {'date': end_time.strftime('%Y-%m-%d')}
  385.             else:
  386.                 # Regular events need datetime with timezone
  387.                 event['start'] = {
  388.                     'dateTime': start_time.isoformat(),
  389.                     'timeZone': 'UTC'  # Default to UTC if no timezone info
  390.                 }
  391.                
  392.                 end_time = ics_event.end.datetime if ics_event.end else (start_time + timedelta(hours=1))
  393.                 event['end'] = {
  394.                     'dateTime': end_time.isoformat(),
  395.                     'timeZone': 'UTC'
  396.                 }
  397.        
  398.         # Handle recurrence
  399.         if hasattr(ics_event, 'rrule') and ics_event.rrule:
  400.             event['recurrence'] = [f"RRULE:{ics_event.rrule}"]
  401.        
  402.         # Handle attendees (if available)
  403.         if hasattr(ics_event, 'attendee') and ics_event.attendee:
  404.             attendees = []
  405.             for attendee in ics_event.attendee:
  406.                 # Extract email from "mailto:[email protected]"
  407.                 if attendee.startswith('mailto:'):
  408.                     email = attendee[7:]
  409.                     attendees.append({'email': email})
  410.            
  411.             if attendees:
  412.                 event['attendees'] = attendees
  413.        
  414.         # Handle reminders (if available)
  415.         if hasattr(ics_event, 'alarms') and ics_event.alarms:
  416.             reminders = {
  417.                 'useDefault': False,
  418.                 'overrides': []
  419.             }
  420.            
  421.             for alarm in ics_event.alarms:
  422.                 if hasattr(alarm, 'trigger') and alarm.trigger:
  423.                     # Convert the trigger time to minutes before event
  424.                     try:
  425.                         # This is a simplification - proper parsing would be more complex
  426.                         if isinstance(alarm.trigger, timedelta):
  427.                             minutes = int(alarm.trigger.total_seconds() / -60)
  428.                             if minutes > 0:
  429.                                 reminders['overrides'].append({
  430.                                     'method': 'popup',
  431.                                     'minutes': minutes
  432.                                 })
  433.                     except Exception:
  434.                         pass
  435.            
  436.             if reminders['overrides']:
  437.                 event['reminders'] = reminders
  438.        
  439.         return event
  440.    
  441.     def import_contacts(self, contacts_dir=None):
  442.         """Import contacts from the Takeout Contacts folder."""
  443.         if contacts_dir:
  444.             contacts_path = Path(contacts_dir)
  445.         else:
  446.             contacts_path = self.takeout_dir / "Contacts"
  447.        
  448.         if not contacts_path.exists():
  449.             print(f"Contacts folder not found at {contacts_path}")
  450.             return
  451.        
  452.         service = self._get_contacts_service()
  453.        
  454.         # Find all .vcf files
  455.         vcf_files = list(contacts_path.glob("**/*.vcf"))
  456.        
  457.         if not vcf_files:
  458.             print("No .vcf files found")
  459.             return
  460.        
  461.         print(f"Found {len(vcf_files)} contact files")
  462.        
  463.         for vcf_file in vcf_files:
  464.             print(f"Processing contact file: {vcf_file}")
  465.            
  466.             try:
  467.                 with open(vcf_file, 'r', encoding='utf-8', errors='ignore') as f:
  468.                     vcf_content = f.read()
  469.                
  470.                 # Parse all vCards in the file
  471.                 for vcard in vobject.readComponents(vcf_content):
  472.                     try:
  473.                         # Convert vCard to People API format
  474.                         person = self._convert_vcard_to_person(vcard)
  475.                        
  476.                         # Create contact
  477.                         for attempt in range(5):
  478.                             try:
  479.                                 service.people().createContact(
  480.                                     body=person
  481.                                 ).execute()
  482.                                 break
  483.                             except Exception as e:
  484.                                 print(f"Error creating contact, attempt {attempt+1}: {e}")
  485.                                 self._exponential_backoff(attempt)
  486.                    
  487.                     except Exception as e:
  488.                         print(f"Error processing contact: {e}")
  489.            
  490.             except Exception as e:
  491.                 print(f"Error reading contact file {vcf_file}: {e}")
  492.        
  493.         print("Contacts import completed!")
  494.    
  495.     def _convert_vcard_to_person(self, vcard):
  496.         """Convert a vCard object to People API person format."""
  497.         person = {}
  498.        
  499.         # Names
  500.         if hasattr(vcard, 'n'):
  501.             names = [{
  502.                 'givenName': vcard.n.value.given or '',
  503.                 'familyName': vcard.n.value.family or '',
  504.                 'middleName': vcard.n.value.additional or '',
  505.                 'honorificPrefix': vcard.n.value.prefix or '',
  506.                 'honorificSuffix': vcard.n.value.suffix or ''
  507.             }]
  508.            
  509.             # If there's a formatted name (FN), use it
  510.             if hasattr(vcard, 'fn'):
  511.                 names[0]['displayName'] = vcard.fn.value
  512.            
  513.             person['names'] = names
  514.        
  515.         # Phone numbers
  516.         if hasattr(vcard, 'tel') and vcard.tel:
  517.             phones = []
  518.             for tel in vcard.tel_list:
  519.                 phone_type = 'other'
  520.                 if hasattr(tel, 'type_param') and tel.type_param:
  521.                     if 'cell' in tel.type_param or 'mobile' in tel.type_param:
  522.                         phone_type = 'mobile'
  523.                     elif 'work' in tel.type_param:
  524.                         phone_type = 'work'
  525.                     elif 'home' in tel.type_param:
  526.                         phone_type = 'home'
  527.                
  528.                 phones.append({
  529.                     'value': tel.value,
  530.                     'type': phone_type
  531.                 })
  532.            
  533.             if phones:
  534.                 person['phoneNumbers'] = phones
  535.        
  536.         # Email addresses
  537.         if hasattr(vcard, 'email') and vcard.email:
  538.             emails = []
  539.             for email in vcard.email_list:
  540.                 email_type = 'other'
  541.                 if hasattr(email, 'type_param') and email.type_param:
  542.                     if 'work' in email.type_param:
  543.                         email_type = 'work'
  544.                     elif 'home' in email.type_param:
  545.                         email_type = 'home'
  546.                
  547.                 emails.append({
  548.                     'value': email.value,
  549.                     'type': email_type
  550.                 })
  551.            
  552.             if emails:
  553.                 person['emailAddresses'] = emails
  554.        
  555.         # Addresses
  556.         if hasattr(vcard, 'adr') and vcard.adr:
  557.             addresses = []
  558.             for adr in vcard.adr_list:
  559.                 address_type = 'other'
  560.                 if hasattr(adr, 'type_param') and adr.type_param:
  561.                     if 'work' in adr.type_param:
  562.                         address_type = 'work'
  563.                     elif 'home' in adr.type_param:
  564.                         address_type = 'home'
  565.                
  566.                 addresses.append({
  567.                     'type': address_type,
  568.                     'streetAddress': adr.value.street,
  569.                     'city': adr.value.city,
  570.                     'region': adr.value.region,
  571.                     'postalCode': adr.value.code,
  572.                     'country': adr.value.country
  573.                 })
  574.            
  575.             if addresses:
  576.                 person['addresses'] = addresses
  577.        
  578.         # Organizations
  579.         if hasattr(vcard, 'org'):
  580.             orgs = [{
  581.                 'name': vcard.org.value[0] if vcard.org.value else '',
  582.                 'title': vcard.title.value if hasattr(vcard, 'title') else ''
  583.             }]
  584.             person['organizations'] = orgs
  585.        
  586.         return person
  587.  
  588.  
  589. def main():
  590.     """Main function to demonstrate usage of the GoogleTakeoutImporter."""
  591.     # Get input from user
  592.     client_secret_file = input("Enter the path to your client_secret.json file: ")
  593.     takeout_dir = input("Enter the path to your extracted Takeout directory: ")
  594.    
  595.     if not os.path.exists(client_secret_file):
  596.         print(f"Error: client_secret.json not found at {client_secret_file}")
  597.         return
  598.    
  599.     if not os.path.exists(takeout_dir):
  600.         print(f"Error: Takeout directory not found at {takeout_dir}")
  601.         return
  602.    
  603.     # Initialize the importer
  604.     importer = GoogleTakeoutImporter(client_secret_file, takeout_dir)
  605.    
  606.     # Ask which services to import
  607.     print("\nWhich data would you like to import?")
  608.     print("1. Google Drive files")
  609.     print("2. Gmail messages")
  610.     print("3. Google Calendar events")
  611.     print("4. Contacts")
  612.     print("5. All of the above")
  613.    
  614.     choice = input("\nEnter your choice (1-5): ")
  615.    
  616.     if choice == '1' or choice == '5':
  617.         importer.import_drive_files()
  618.    
  619.     if choice == '2' or choice == '5':
  620.         importer.import_gmail_messages()
  621.    
  622.     if choice == '3' or choice == '5':
  623.         importer.import_calendar_events()
  624.    
  625.     if choice == '4' or choice == '5':
  626.         importer.import_contacts()
  627.    
  628.     print("\nImport process completed!")
  629.  
  630.  
  631. if __name__ == "__main__":
  632.     main()
Add Comment
Please, Sign In to add comment