Vérifier la reprise d’un téléchargement iOS sur un Mac distant

Vérifier la reprise d’un téléchargement iOS sur un Mac distant

Lors du développement à distance d’une App iOS qui doit télécharger des ressources hors ligne volumineuses, une barre de progression qui repart de sa position précédente après une coupure réseau ne prouve pas que le client réutilise les octets déjà reçus. Il peut tout aussi bien recommencer le téléchargement en conservant l’ancien avancement à l’écran. Sur un Mac dans le cloud, on peut faire accéder le simulateur à un serveur local hébergé sur la même machine, puis vérifier le comportement de manière reproductible à l’aide des journaux du serveur et d’un contrôle du fichier. Aucun service de fichiers accessible sur Internet n’est nécessaire.

Définir les critères de réussite

Ce test porte sur un seul fichier et une seule plage d’octets ; il ne couvre pas les téléchargements parallèles par segments. Préparez un fichier sans données sensibles, assez volumineux pour pouvoir interrompre le téléchargement en cours. La première requête doit recevoir 200. Si le client conserve des informations de reprise valides, une requête suivante peut contenir Range: bytes=N- ; le serveur répond alors avec 206 et un Content-Range exact. La taille et l’empreinte SHA-256 du fichier final doivent correspondre à celles du fichier source.

resumeData contient les données dont le client a besoin pour tenter de reprendre le téléchargement. Ce n’est pas la preuve que le serveur a déjà fourni une réponse partielle. Une annulation ne produit pas non plus systématiquement des resumeData utilisables.

Créez d’abord le fichier dans un répertoire de test dédié et relevez les valeurs de référence. Sa taille sert uniquement à laisser le temps d’annuler manuellement le téléchargement ; elle ne représente ni une configuration produit ni un indicateur de performance :

mkdir -p ~/range-lab && cd ~/range-lab
python3 - <<'PY'
with open("fixture.bin", "wb") as out:
    for _ in range(64):
        out.write(b"XCZone-range-check" * 65536)
PY
shasum -a 256 fixture.bin
stat -f '%z bytes' fixture.bin

Mettre en place un serveur Range accessible uniquement en local

Ce serveur minimal ne distribue que le fichier fixture.bin du répertoire courant. Il génère un ETag pour le fichier et permet de lire depuis un décalage donné jusqu’à la fin ; il répond 416 aux requêtes Range non prises en charge. Une courte pause après l’envoi de chaque bloc facilite l’interruption dans le simulateur. Ce n’est pas un serveur de fichiers destiné à être exposé publiquement.

from hashlib import sha256
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from time import sleep
import re

path = Path("fixture.bin")
size = path.stat().st_size
etag = '"' + sha256(path.read_bytes()).hexdigest() + '"'

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path != "/fixture.bin":
            self.send_error(404)
            return
        requested = self.headers.get("Range")
        if self.headers.get("If-Range") not in (None, etag):
            requested = None
        start = 0
        if requested:
            match = re.fullmatch(r"bytes=(\d+)-", requested)
            if not match or int(match.group(1)) >= size:
                self.send_response(416)
                self.send_header("Content-Range", f"bytes */{size}")
                self.end_headers()
                return
            start = int(match.group(1))
        self.send_response(206 if requested else 200)
        self.send_header("Content-Type", "application/octet-stream")
        self.send_header("Accept-Ranges", "bytes")
        self.send_header("ETag", etag)
        self.send_header("Content-Length", str(size - start))
        if requested:
            self.send_header("Content-Range", f"bytes {start}-{size - 1}/{size}")
        self.end_headers()
        with path.open("rb") as source:
            source.seek(start)
            while chunk := source.read(65536):
                try:
                    self.wfile.write(chunk)
                    self.wfile.flush()
                except (BrokenPipeError, ConnectionResetError):
                    break
                sleep(0.01)

HTTPServer(("127.0.0.1", 8765), Handler).serve_forever()

Enregistrez le code dans range_server.py, puis exécutez python3 range_server.py depuis ce répertoire. Dans un autre terminal, lancez d’abord curl -i -r 1024-2047 http://127.0.0.1:8765/fixture.bin -o /dev/null pour vérifier le code de statut et les en-têtes de réponse. La sortie peut afficher la progression du transfert ; ne vous en servez pas pour juger du contenu. Vous pouvez utiliser curl -sD - -r 1024-2047 -o /dev/null http://127.0.0.1:8765/fixture.bin pour contrôler explicitement 206, Content-Range: bytes 1024-2047/… et Content-Length. À la fin du test, arrêtez le serveur au clavier et ne remplacez pas l’adresse d’écoute par celle d’une interface réseau accessible de l’extérieur.

Interrompre et reprendre le téléchargement dans le simulateur

Configurez la version de test de l’App pour qu’elle télécharge http://127.0.0.1:8765/fixture.bin avec URLSessionDownloadTask. Le simulateur et le Mac qui l’exécute partagent ce chemin d’accès local. Sur un appareil physique, en revanche, 127.0.0.1 désigne le téléphone : cette adresse ne peut pas être reprise telle quelle. Si une exception est nécessaire pour autoriser le HTTP non chiffré, définissez les autorisations de sécurité réseau uniquement dans une configuration de test distincte et vérifiez que la version de production ne contient pas cette exception.

Une fois le téléchargement lancé, appelez cancel(byProducingResumeData:), conservez les données reçues dans le rappel, puis créez une nouvelle tâche avec downloadTask(withResumeData:). Gardez la sortie du terminal du serveur et notez, pour chaque requête, la plage Range, le statut de la réponse et la taille du fichier final. Vérifiez d’abord qu’un téléchargement sans interruption aboutit avant de tester la reprise ; sinon, les problèmes de connexion risquent d’être confondus avec ceux de la reprise.

Distinguer une reprise d’un nouveau téléchargement

Si la deuxième requête ne contient pas de Range et que le serveur répond 200, le client a peut-être choisi de tout retélécharger : ce n’est pas une reprise partielle réussie. Si la requête contient Range mais reçoit une réponse complète parce que l’ETag du fichier a changé, il faut également réécrire le fichier depuis le début. Dans tous les cas, il ne faut jamais ajouter une réponse complète à la fin de l’ancien fragment. Le code de test doit aussi couvrir le cas où l’annulation ne fournit pas de resumeData : il doit permettre de recommencer explicitement, plutôt que de laisser la tâche « en attente de reprise ».

Confirmer le résultat par une vérification du fichier

Une fois la tâche de téléchargement terminée, copiez son fichier temporaire dans le répertoire de test, puis exécutez shasum -a 256 下载后的文件路径. Comparez l’empreinte caractère par caractère avec celle du fichier source et vérifiez également la taille. Un 206 indique seulement que le serveur a renvoyé une partie des données. Si les empreintes diffèrent, examinez le décalage, la méthode d’écriture et une éventuelle modification du fichier entre les deux requêtes.

Terminez par un contre-exemple : interrompez le téléchargement, modifiez le fichier source, puis redémarrez le serveur de test afin de changer l’ETag. Le client ne doit pas traiter l’ancien fragment comme le début du nouveau fichier. Regrouper les requêtes, les codes de statut, les tailles et les empreintes dans un même compte rendu de test facilite le diagnostic des régressions bien mieux qu’une simple capture de la barre de progression.

Questions fréquentes

Une réponse 206 suffit-elle à valider la reprise ?

Non. Vérifiez aussi le point de départ demandé, la taille finale et l’empreinte SHA-256.

Que faire si le serveur répond 200 à la relance ?

Traitez la réponse comme un fichier complet et repartez de zéro. Ne l’ajoutez pas au fragment existant.

XCZone · Mac dans le cloud

Besoin d’un Mac physique dédié accessible à distance ?

Comparez les configurations, les emplacements et les périodes de facturation de M4 Start, M4 Work et M4 Pro, puis choisissez l’offre adaptée à vos besoins.

Découvrir les offres de Mac dans le cloud