Skip to content

Commit

Permalink
Merge pull request #1259 from doronz88/feature/restore-applications
Browse files Browse the repository at this point in the history
Feature/restore applications
  • Loading branch information
doronz88 authored Oct 23, 2024
2 parents 487797b + ebe95ac commit 5b64a02
Show file tree
Hide file tree
Showing 4 changed files with 29 additions and 11 deletions.
11 changes: 7 additions & 4 deletions pymobiledevice3/cli/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from pymobiledevice3.cli.cli_common import Command
from pymobiledevice3.lockdown import LockdownClient
from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
from pymobiledevice3.services.mobilebackup2 import Mobilebackup2Service

source_option = click.option('--source', default='', help='The UDID of the source device.')
Expand All @@ -31,7 +32,7 @@ def backup2() -> None:
@click.argument('backup-directory', type=click.Path(file_okay=False))
@click.option('--full', is_flag=True, help=('Whether to do a full backup.'
' If full is True, any previous backup attempts will be discarded.'))
def backup(service_provider: LockdownClient, backup_directory, full):
def backup(service_provider: LockdownServiceProvider, backup_directory: str, full: bool) -> None:
"""
Backup device.
Expand All @@ -50,12 +51,14 @@ def update_bar(percentage):
@backup_directory_arg
@click.option('--system/--no-system', default=False, help='Restore system files.')
@click.option('--reboot/--no-reboot', default=True, help='Reboot the device when done.')
@click.option('--copy/--no-copy', default=True, help='Create a copy of backup folder before restoring.')
@click.option('--copy/--no-copy', default=False, help='Create a copy of backup folder before restoring.')
@click.option('--settings/--no-settings', default=True, help='Restore device settings.')
@click.option('--remove/--no-remove', default=False, help='Remove items which aren\'t being restored.')
@click.option('--skip-apps', is_flag=True, help='Do not trigger re-installation of apps after restore')
@password_option
@source_option
def restore(service_provider: LockdownClient, backup_directory, system, reboot, copy, settings, remove, password, source):
def restore(service_provider: LockdownServiceProvider, backup_directory: str, system: bool, reboot: bool, copy: bool,
settings: bool, remove: bool, skip_apps: bool, password: str, source: str) -> None:
"""
Restore a backup to a device.
Expand All @@ -69,7 +72,7 @@ def update_bar(percentage):

backup_client.restore(backup_directory=backup_directory, progress_callback=update_bar, system=system,
reboot=reboot, copy=copy, settings=settings, remove=remove, password=password,
source=source)
source=source, skip_apps=skip_apps)


@backup2.command(cls=Command)
Expand Down
6 changes: 5 additions & 1 deletion pymobiledevice3/lockdown_service_provider.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import datetime
import logging
from abc import abstractmethod
from typing import Optional
from typing import Any, Optional

from pymobiledevice3.exceptions import StartServiceError
from pymobiledevice3.service_connection import ServiceConnection
Expand Down Expand Up @@ -45,6 +45,10 @@ async def aio_start_lockdown_service(
self, name: str, include_escrow_bag: bool = False) -> ServiceConnection:
pass

@abstractmethod
def get_value(self, domain: Optional[str] = None, key: Optional[str] = None) -> Any:
pass

def start_lockdown_developer_service(
self, name: str, include_escrow_bag: bool = False) -> ServiceConnection:
try:
Expand Down
4 changes: 2 additions & 2 deletions pymobiledevice3/remote/remote_service_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging
from dataclasses import dataclass
from datetime import datetime
from typing import Optional, Union
from typing import Any, Optional, Union

from pymobiledevice3.bonjour import DEFAULT_BONJOUR_TIMEOUT, browse_remoted
from pymobiledevice3.common import get_home_folder
Expand Down Expand Up @@ -67,7 +67,7 @@ async def connect(self) -> None:
self.start_lockdown_service('com.apple.mobile.lockdown.remote.untrusted'))
self.all_values = self.lockdown.all_values

def get_value(self, domain: str = None, key: str = None):
def get_value(self, domain: Optional[str] = None, key: Optional[str] = None) -> Any:
return self.lockdown.get_value(domain, key)

def start_lockdown_service_without_checkin(self, name: str) -> ServiceConnection:
Expand Down
19 changes: 15 additions & 4 deletions pymobiledevice3/services/mobilebackup2.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from pymobiledevice3.exceptions import AfcException, AfcFileNotFoundError, ConnectionTerminatedError, LockdownError, \
PyMobileDevice3Exception
from pymobiledevice3.lockdown import LockdownClient
from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
from pymobiledevice3.services.afc import AFC_LOCK_EX, AFC_LOCK_UN, AfcService, afc_error_t
from pymobiledevice3.services.device_link import DeviceLink
from pymobiledevice3.services.installation_proxy import InstallationProxyService
Expand All @@ -32,20 +33,20 @@ class Mobilebackup2Service(LockdownService):
SERVICE_NAME = 'com.apple.mobilebackup2'
RSD_SERVICE_NAME = 'com.apple.mobilebackup2.shim.remote'

def __init__(self, lockdown: LockdownClient):
def __init__(self, lockdown: LockdownServiceProvider) -> None:
if isinstance(lockdown, LockdownClient):
super().__init__(lockdown, self.SERVICE_NAME, include_escrow_bag=True)
else:
super().__init__(lockdown, self.RSD_SERVICE_NAME, include_escrow_bag=True)

@property
def will_encrypt(self):
def will_encrypt(self) -> bool:
try:
return self.lockdown.get_value('com.apple.mobile.backup', 'WillEncrypt')
except LockdownError:
return False

def backup(self, full: bool = True, backup_directory='.', progress_callback=lambda x: None) -> None:
def backup(self, full: bool = True, backup_directory: str = '.', progress_callback=lambda x: None) -> None:
"""
Backup a device.
:param full: Whether to do a full backup. If full is True, any previous backup attempts will be discarded.
Expand Down Expand Up @@ -92,7 +93,7 @@ def backup(self, full: bool = True, backup_directory='.', progress_callback=lamb

def restore(self, backup_directory='.', system: bool = False, reboot: bool = True, copy: bool = True,
settings: bool = True, remove: bool = False, password: str = '', source: str = '',
progress_callback=lambda x: None):
progress_callback=lambda x: None, skip_apps: bool = False):
"""
Restore a previous backup to the device.
:param backup_directory: Path of the backup directory.
Expand All @@ -104,6 +105,7 @@ def restore(self, backup_directory='.', system: bool = False, reboot: bool = Tru
:param password: Password of the backup if it is encrypted.
:param source: Identifier of device to restore its backup.
:param progress_callback: Function to be called as the backup progresses.
:param skip_apps: Do not trigger re-installation of apps after restore.
The function shall receive the current percentage of the progress as a parameter.
"""
backup_directory = Path(backup_directory)
Expand Down Expand Up @@ -137,6 +139,15 @@ def restore(self, backup_directory='.', system: bool = False, reboot: bool = Tru
'SourceIdentifier': source,
'Options': options,
})

if not skip_apps:
# Write /iTunesRestore/RestoreApplications.plist so that the device will start
# restoring applications once the rest of the restore process is finished
info_plist_path = backup_directory / source / 'Info.plist'
applications = plistlib.loads(info_plist_path.read_bytes())['Applications']
afc.makedirs('/iTunesRestore')
afc.set_file_contents('/iTunesRestore/RestoreApplications.plist', plistlib.dumps(applications))

dl.dl_loop(progress_callback)

def info(self, backup_directory='.', source: str = '') -> str:
Expand Down

0 comments on commit 5b64a02

Please sign in to comment.