Aller au contenu
English

Modèles de stockage objet

  • Leçon 14
  • 55 min
  • Module 5 : Fichiers, vecteurs et cycle de vie

Ce que vous saurez faire

Conservez les photos et les reçus joints par les clients dans le stockage objet, et leurs détails dans PostgreSQL. Laissez les clients téléverser et télécharger chaque fichier avec un lien autorisant une seule action sur un fichier pendant quelques minutes. Refusez les fichiers trop volumineux.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 55 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

  • Le projet ~/projects/ticket-api à la fin du module 4. python -m pytest affiche 87 passed (87 tests réussis).
  • PostgreSQL en marche, et le dépôt des exercices dans ~/projects. L’étape 5 recharge les petites données à la place des grandes données du module 4. Si vous avez cloné le dépôt il y a quelque temps, exécutez d’abord git -C ../nextia-learning-labs pull.
  • Docker, pour l’émulateur de stockage objet. Sans Docker, consultez la note à l’étape 4.

Cette leçon ajoute le stockage objet à Compose, une migration (009_attachment_uploads.sql), deux nouveaux modules (files.py, attachments.py) et un fichier de tests. Elle modifie aussi six fichiers de l’API.

Lors d’un concert, vous laissez votre manteau au vestiaire. La personne au comptoir le suspend au crochet 214 et vous donne un petit ticket portant le numéro 214. Le manteau reste sur le crochet; le ticket va dans votre poche. La salle de concert ne garde pas les manteaux sur les sièges.

Le ticket remplit trois fonctions utiles. Il désigne un seul crochet, donc il ne permet pas de récupérer un autre manteau. Il autorise une seule action : « donnez-moi mon manteau ». Et le vestiaire ferme à minuit. Après, le ticket ne vaut plus rien.

Le problème : 43 lignes et aucun endroit pour les fichiers

Section intitulée « Le problème : 43 lignes et aucun endroit pour les fichiers »

Le service de soutien décrit déjà ses fichiers. Ouvrez psql avec les petites données et examinez le billet T-30034, concernant le colis arrivé endommagé :

SELECT ticket_id, file_name, content_type, size_bytes, object_key
FROM attachments WHERE ticket_id = 'T-30034';
ticket_id | file_name | content_type | size_bytes | object_key
-----------+-------------+--------------+------------+------------------------------------------------------------------
T-30034 | photo-1.png | image/png | 3613 | tickets/T-30034/fcf3867b-6929-4624-9e25-c526ce2f3e67/photo-1.png
T-30034 | photo-2.png | image/png | 3767 | tickets/T-30034/d38ab247-9828-4dce-a1ee-b0b2e6baa45e/photo-2.png
(2 rows)

Les lignes indiquent le nom de chaque fichier et son emplacement, object_key. Mais cet emplacement n’existe pas encore. Les 43 fichiers des petites données se trouvent seulement dans un dossier du projet, data/small/files/. Aucun programme ne peut téléverser une nouvelle photo, et aucun agent ne peut en ouvrir une.

Amira énumère les premières idées de l’équipe et le coût de chacune :

  • Mettre les octets dans une colonne bytea de PostgreSQL. Cela fonctionne pour quelques reçus. Mais chaque photo passe alors par l’API, le pool de connexions et chaque sauvegarde. Avec un an de photos, une base de données de 9 Mo atteint plusieurs gigaoctets, et chaque restauration prend plus de temps.
  • Garder les fichiers sur le disque du serveur de l’API. L’API fonctionne dans un conteneur. Lors de sa reconstruction, le disque peut disparaître avec lui, comme l’a montré Données persistantes. Un deuxième serveur de l’API ne verrait pas les fichiers du premier.
  • Envoyer les octets vers un stockage en passant par l’API. L’API mobilise alors un processus pour chaque téléversement lent depuis un téléphone.

L’idée : le stockage objet, avec les détails dans PostgreSQL

Section intitulée « L’idée : le stockage objet, avec les détails dans PostgreSQL »

Le stockage objet garde des fichiers, appelés objets, dans un espace sans hiérarchie. Chaque objet a une clé, un nom comme tickets/T-30034/fcf3867b-…/photo-1.png. Vous déposez un objet, le récupérez ou le supprimez, toujours avec sa clé. Les objets se trouvent dans un conteneur (Amazon S3 l’appelle un compartiment, ou bucket). Le stockage objet n’a ni tables, ni jointures, ni contraintes, et son coût par gigaoctet est faible. Azure Blob Storage, Amazon S3 et Google Cloud Storage sont des services de stockage objet.

C’est le vestiaire. Les crochets portent les manteaux; le ticket porte le numéro du crochet. De même, PostgreSQL garde une ligne par fichier : son billet, son nom, son type, sa taille, sa somme de contrôle et sa clé. Le stockage objet garde les octets. La ligne est le ticket du vestiaire, et object_key est le numéro du crochet.

Quoi Où Pour la première photo de T-30034
Les octets de la photo stockage objet 3 613 octets de PNG
Le billet auquel elle appartient PostgreSQL, attachments.ticket_id T-30034
Sa clé dans le stockage objet PostgreSQL, attachments.object_key tickets/T-30034/fcf3867b-…/photo-1.png
Sa taille et sa somme de contrôle PostgreSQL, size_bytes, sha256 3613, 91c6510a03d4…

La clé contient l’UUID de la pièce jointe. Ainsi, deux photos nommées photo-1.png n’ont jamais la même clé. Personne ne peut deviner une clé avec seulement un numéro de billet.

Sur votre ordinateur, le cours utilise Azurite, l’émulateur Azure Blob Storage de Microsoft (version 3.37.0, licence MIT). Il fonctionne dans Docker et répond aux mêmes requêtes que le service infonuagique, sans compte ni frais. L’API communique avec lui au moyen du paquet azure-storage-blob de Microsoft (12.31.0).

Les URL signées : un ticket de vestiaire pour un fichier

Section intitulée « Les URL signées : un ticket de vestiaire pour un fichier »

L’API ne devrait pas transporter les octets. Mais le client ne peut pas non plus avoir la clé du compte de stockage. Avec cette clé, un téléphone pourrait lire, modifier ou supprimer les fichiers de tous les clients.

La solution est une URL signée : un lien autorisant une action sur un objet jusqu’à une heure donnée, et rien d’autre. L’API la crée avec la clé du compte, et le client ne voit jamais cette clé. Azure l’appelle une SAS (signature d’accès partagé). Amazon S3 l’appelle une URL présignée. Voici l’URL de téléchargement d’une exécution du cours, en quatre parties :

Une URL signée a quatre parties. Quel fichier : .../attachments/tickets/T-30002/34cf7dbf.../photo-1.png. Jusqu'à quand : se=2026-10-10T04:18:10Z. Quelle action : sp=r pour lire, sp=cw pour créer et écrire. Signature : sig=9g6WH5O85A..., créée avec la clé du compte. Cinq essais avec l'URL de téléchargement de la leçon : la même URL avant son expiration donne 200 et 3390 octets; un autre nom de fichier avec la même signature donne 403; la même URL après son expiration donne 403; une URL de lecture utilisée pour téléverser donne 403; une URL de téléversement utilisée pour télécharger donne 403.
La signature est calculée à partir des autres parties avec une clé secrète. Un nom de fichier modifié, une heure plus tardive ou une autre action la rend donc incorrecte, et le stockage répond 403. Les cinq essais sont des exécutions réelles.
Description textuelle du diagramme

Une URL signée a quatre parties. Le chemin désigne un fichier : …/attachments/tickets/T-30002/34cf7dbf…/photo-1.png. Le paramètre se donne l’heure d’expiration, 2026-10-10T04:18:10Z. Le paramètre sp donne l’action : r pour lire, ou cw pour créer et écrire. Le paramètre sig est la signature, créée à partir des autres parties avec la clé du compte. Le diagramme présente cinq essais réels. La même URL avant son expiration donne 200 et 3390 octets. Un autre nom de fichier avec la même signature donne 403. La même URL après son expiration donne 403. Une URL de lecture utilisée pour téléverser donne 403. Une URL de téléversement utilisée pour télécharger donne 403.

Sam demande : « Quelqu’un qui voit le lien peut-il remplacer photo-1.png par photo-2.png? » Il peut modifier le texte, mais la signature a été créée pour photo-1.png. Le stockage recalcule la signature du lien modifié, trouve une valeur différente et refuse avec AuthorizationFailure (échec d’autorisation). Sans la clé du compte, personne ne peut créer une signature correspondante.

Vous pouvez donc transmettre le lien à un navigateur pour une courte période sans danger. Il n’est pas sûr de le consigner dans les journaux, de le coller dans un billet ou de le garder une semaine. Toute personne qui le possède peut l’utiliser jusqu’à son expiration. C’est pourquoi le projet accorde 10 minutes aux liens de téléversement et 5 minutes aux liens de téléchargement.

Mei et Amira conçoivent le déroulement pour que l’API ne touche jamais aux octets et que la base de données ne fasse jamais confiance au client :

Une séquence entre trois parties : le client, ticket-api avec PostgreSQL et le stockage objet (Azurite). 1 : le client envoie POST /v1/tickets/T-30002/attachments avec photo-1.png, image/png, 3390 octets. L'API vérifie que la taille ne dépasse pas 10 Mio, insère une ligne avec l'état pending (en attente), et renvoie une URL de téléversement avec sp=cw pour 10 minutes. 2 : le client envoie le fichier à l'URL de téléversement par PUT et reçoit 201; le fichier ne passe jamais par l'API. 3 : le client envoie POST /v1/attachments/{id}/complete. L'API demande au stockage la taille du fichier et le lit; le stockage répond 3390 octets. Si la taille correspond à celle annoncée et ne dépasse pas 10 Mio, la ligne passe à stored (stocké), avec son SHA-256. Si elle dépasse 10 Mio, l'API supprime le fichier, la ligne passe à rejected (refusé), et la réponse est 413. 4 : le client demande GET /v1/attachments/{id}/download et reçoit une URL de téléchargement avec sp=r pour 5 minutes. Il récupère ensuite le fichier du stockage par GET : 200, 3390 octets.
Quatre requêtes. L'API décide; le stockage objet transporte les octets. La ligne commence à l'état pending et passe à stored seulement après que l'API a vérifié le fichier réel.
Description textuelle du diagramme

Étape 1 : le client demande à l’API de commencer le téléversement de photo-1.png, image/png, 3390 octets, pour le billet T-30002. L’API vérifie la limite de taille, insère une ligne avec l’état pending et renvoie une URL autorisant la création et l’écriture pendant 10 minutes. Étape 2 : le client envoie le fichier directement au stockage objet avec cette URL et reçoit 201. Le fichier ne passe jamais par l’API. Étape 3 : le client demande à l’API de terminer le téléversement. L’API demande au stockage la taille réelle, 3390 octets, et lit le fichier pour calculer son SHA-256. Si la taille correspond à celle annoncée et ne dépasse pas 10 Mio, la ligne passe à stored. Si le fichier dépasse 10 Mio, l’API le supprime, la ligne passe à rejected, et la réponse est 413. Étape 4 : le client demande une URL de téléchargement, qui autorise la lecture pendant 5 minutes, et récupère le fichier du stockage. L’API ne garde aucune connexion à la base de données pendant ses échanges avec le stockage objet.

Trois décisions méritent un examen plus attentif.

La ligne vient d’abord, à l’état pending (en attente). La migration 009 ajoute une colonne status avec trois valeurs : pending, stored (stocké) et rejected (refusé). Un CHECK (contrôle) exige qu’un fichier stocké ait une somme de contrôle et une heure de téléversement. La base de données ne peut donc pas déclarer un fichier que personne n’a vérifié. Une ligne en attente dont le fichier n’arrive jamais est sans danger : la page du billet affiche seulement les fichiers stockés.

L’opération de fin vérifie le fichier réel, pas la parole du client. Le client a annoncé 3 390 octets. À complete (terminer), l’API demande la taille au stockage, lit les octets et calcule le SHA-256. La ligne change seulement après, avec un seul UPDATE … WHERE status = 'pending'. Terminer deux fois donne la même réponse et ne change rien.

Aucune connexion n’est gardée pendant les appels au stockage. Dans Gestion des connexions, un pool de 10 connexions servait l’API. Un appel au stockage peut prendre plusieurs secondes sur un réseau lent. Alors, attachments.py emprunte une connexion pour une courte lecture, puis la rend. Ensuite, il communique avec le stockage, puis emprunte de nouveau une connexion pour une courte écriture :

ticket_api/attachments.py (complete_upload, shortened)
with db.connection() as conn: # 1. a short read
row = repository.get_attachment(conn, str(attachment_id))
size = files.size(row["object_key"]) # 2. the object storage, with no connection held
...
with db.connection() as conn: # 3. a short write, only if the row is still pending
updated = repository.finish_attachment(conn, str(attachment_id), stored=problem is None,
size_bytes=size, sha256=sha)

Derrière files se trouve un petit contrat, FileStore dans ticket_api/files.py : URL de téléversement, URL de téléchargement, taille, lecture, dépôt, suppression, liste des clés. AzureBlobStore communique avec Azurite ou Azure. LocalFileStore garde les fichiers dans un dossier et signe ses propres liens, donc les tests n’ont pas besoin de Docker. Un autre service infonuagique demanderait une troisième classe, et rien d’autre ne changerait dans l’API.

Les limites de taille : ce qu’une URL signée ne peut pas garantir

Section intitulée « Les limites de taille : ce qu’une URL signée ne peut pas garantir »

Le service de soutien garde des fichiers jusqu’à 10 Mio (10 485 760 octets). L’API refuse immédiatement une taille annoncée supérieure, avec 413 file_too_large (fichier trop volumineux). Mais un client peut annoncer 1 000 octets, puis envoyer un fichier beaucoup plus gros. Le cours l’a essayé avec un lien créé pour un fichier de 1 000 octets :

PUT 12,000,000 bytes to the upload URL -> 201
POST /v1/attachments/<id>/complete -> 413
{"error":{"code":"file_too_large","message":"The file is larger than 10485760 bytes.","request_id":"21877b981272","fields":[]}}

Une SAS Azure n’impose aucune condition de taille. Le stockage a accepté 12 Mo, car le lien autorise à « créer et écrire cette clé », sans limite. Le projet vérifie donc la taille à complete, supprime le fichier et marque la ligne rejected. Le fichier est resté dans le stockage quelques secondes. Un client qui n’appelle jamais complete l’y laisse. Le balayage dans Restauration et conservation trouve ces vieux téléversements en attente.

Amazon S3 propose un autre outil. Un POST présigné contient une politique que S3 vérifie pendant le téléversement, dont une condition content-length-range (plage de taille du contenu). Par exemple, elle peut autoriser de 1 à 10 485 760 octets. S3 refuse un fichier plus gros avant de le stocker. Ce cours décrit cet outil sans l’exécuter, car il exige un compte S3 ou un autre émulateur. Dans les deux cas, gardez la vérification finale de votre côté. La migration 009 ajoute aussi CHECK (size_bytes <= 10485760), donc aucun programme ne peut enregistrer un fichier plus gros.

Les règles de cycle de vie : le stockage fait le ménage lui-même

Section intitulée « Les règles de cycle de vie : le stockage fait le ménage lui-même »

Les fichiers ont aussi une durée de vie. Un téléversement refusé devient inutile après une heure. Le plan de conservation de Grace, dans la dernière leçon de ce module, peut prévoir de « supprimer les photos des billets fermés après deux ans ». Les services infonuagiques peuvent accomplir une partie de ce travail eux-mêmes, avec des règles de cycle de vie. Une règle de cycle de vie se trouve dans le compte de stockage. Elle supprime les objets correspondant à un préfixe et à un âge, ou les déplace vers un niveau moins coûteux.

Azurite n’exécute pas les règles de cycle de vie. L’exemple ci-dessous suit donc la structure de la documentation Azure, et personne ne l’a exécuté dans ce cours. Il supprime les objets sous attachments/uploads-tmp/ 2 jours après leur dernière modification :

An Azure lifecycle rule (from the documentation's structure; not run)
{
"rules": [
{
"name": "delete-unfinished-uploads",
"enabled": true,
"type": "Lifecycle",
"definition": {
"filters": { "blobTypes": ["blockBlob"], "prefixMatch": ["attachments/uploads-tmp/"] },
"actions": { "baseBlob": { "delete": { "daysAfterModificationGreaterThan": 2 } } }
}
}
]
}

Trois faits de la documentation comptent pour établir un plan. Une nouvelle règle peut prendre jusqu’à 24 heures avant sa première exécution. Les conditions sont des âges en jours : depuis la création, la dernière modification, ou la dernière lecture si le suivi des accès est activé. Un filtre peut seulement inclure des objets selon un préfixe ou une étiquette; il ne peut en exclure aucun.

Une règle de cycle de vie ne connaît rien de votre base de données. Elle voit des âges et des préfixes, pas « ce billet est fermé ». Utilisez-la donc pour des groupes entiers d’objets dont la base de données n’a plus besoin, sous leur propre préfixe. Supprimez les fichiers d’un client précis depuis votre programme, comme le fait la dernière leçon de ce module.

Exercice guidé : ajouter le stockage objet à ticket-api

Section intitulée « Exercice guidé : ajouter le stockage objet à ticket-api »
  1. Dans ~/projects/ticket-api, copiez les fichiers de cette leçon depuis l’instantané end-of-m05. Sur macOS ou Linux :

    Fenêtre de terminal
    S=../nextia-learning-labs/C20/snapshots/end-of-m05
    cp $S/compose.yaml $S/.env.example $S/pyproject.toml $S/requirements*.txt .
    cp $S/contract/openapi.json contract/
    cp $S/ticket_api/{config,errors,main,models,tickets,repository,files,attachments}.py ticket_api/
    cp $S/migrations/009_attachment_uploads.sql migrations/
    cp $S/tests/test_files.py tests/

    Sur Windows, dans PowerShell (commandes écrites, mais non testées) :

    Fenêtre de terminal
    $S = "..\nextia-learning-labs\C20\snapshots\end-of-m05"
    Copy-Item "$S\compose.yaml", "$S\.env.example", "$S\pyproject.toml", "$S\requirements*.txt" .
    Copy-Item "$S\contract\openapi.json" contract\
    foreach ($f in "config","errors","main","models","tickets","repository","files","attachments") { Copy-Item "$S\ticket_api\$f.py" ticket_api\ }
    Copy-Item "$S\migrations\009_attachment_uploads.sql" migrations\
    Copy-Item "$S\tests\test_files.py" tests\

    Les fichiers copiés tickets.py et repository.py contiennent aussi la recherche de billets similaires. Elle reste inutilisée jusqu’à Stockage des vecteurs.

  2. Ouvrez .env.example. Copiez ses trois dernières lignes, qui commencent par # Object storage (stockage objet), à la fin de votre .env. Le nom de compte devstoreaccount1 et sa longue clé sont les identifiants de test publics d’Azurite. Microsoft les publie, et toutes les instances d’Azurite utilisent les mêmes. Ils ne sont donc pas secrets. La clé d’un vrai compte de stockage est secrète : ne la mettez jamais dans un fichier enregistré par un commit (enregistrement dans le dépôt).

  3. Installez le nouveau paquet (il inclut azure-storage-blob 12.31.0) :

    Fenêtre de terminal
    python -m pip install -r requirements.txt
  4. Démarrez Azurite à côté de PostgreSQL :

    Fenêtre de terminal
    docker compose up -d db azurite

    Sans Docker, Azurite existe aussi comme paquet npm (npm install -g azurite, puis azurite-blob --skipApiVersionCheck). Le cours n’a pas testé cette méthode.

  5. Rechargez le petit jeu de données. Le programme de chargement applique la migration 009 et place les 43 fichiers dans le stockage objet :

    Fenêtre de terminal
    python -m scripts.load --reset
    applied 009_attachment_uploads
    Database is up to date (9 migrations).
    Loaded the small data in 0.5 s:
    customers 40
    tickets 200
    messages 331
    attachments 43
    documents 36
    ai_runs 294
    ticket_embeddings 200
    43 files uploaded to the object storage.
    3 messages have no ticket: they are in orphaned_messages.
  6. Exécutez les tests. Ils utilisent LocalFileStore et n’ont donc pas besoin d’Azurite :

    Fenêtre de terminal
    python -m pytest

    La dernière ligne est 95 passed, 1 deselected (95 tests réussis, 1 non sélectionné). Le test non sélectionné communique avec Azurite. Exécutez-le seul avec python -m pytest -m azurite : 1 passed, 95 deselected (1 test réussi, 95 non sélectionnés).

  7. Démarrez l’API avec fastapi dev. Dans un deuxième terminal, dans le dossier du projet, copiez une photo du petit jeu de données vers photo-1.png :

    Fenêtre de terminal
    cp data/small/files/tickets/T-30014/*/photo-2.png photo-1.png

    Sur Windows : Copy-Item data\small\files\tickets\T-30014\*\photo-2.png photo-1.png. Le fichier contient 3 390 octets. Si votre .env définit API_KEY, ajoutez -H "X-API-Key: <your key>" à chaque requête vers le port 8000 ci-dessous.

  8. Commencez un téléversement pour le billet T-30002 :

    Fenêtre de terminal
    curl -s -X POST http://127.0.0.1:8000/v1/tickets/T-30002/attachments \
    -H 'Content-Type: application/json' \
    -d '{"file_name": "photo-1.png", "content_type": "image/png", "size_bytes": 3390}'
    {"attachment_id":"34cf7dbf-8b5f-4b2b-ab87-1776f335a986","upload_url":"http://127.0.0.1:10000/devstoreaccount1/attachments/tickets/T-30002/34cf7dbf-8b5f-4b2b-ab87-1776f335a986/photo-1.png?se=2026-10-10T04%3A23%3A10Z&sp=cw&sv=2026-10-06&sr=b&sig=6QSxque36Itx%2B5gphGJKF0DCanEaJ8VtNAXL4MzTi8s%3D","method":"PUT","headers":{"x-ms-blob-type":"BlockBlob","Content-Type":"image/png"},"expires_at":"2026-10-10T04:23:10Z","max_bytes":10485760}

    Votre identifiant, vos heures et votre signature seront différents. Copiez deux valeurs dans des variables du shell, avec vos propres valeurs : ID=34cf7dbf-… et URL='http://127.0.0.1:10000/…'. Gardez les apostrophes autour de l’URL, car elle contient &.

  9. Regardez la ligne dans psql : SELECT file_name, size_bytes, status, sha256 FROM attachments WHERE ticket_id = 'T-30002';. Elle affiche photo-1.png | 3390 | pending | (en attente), sans somme de contrôle. Demandez à l’API de terminer maintenant, avant le téléversement :

    Fenêtre de terminal
    curl -s -X POST http://127.0.0.1:8000/v1/attachments/$ID/complete
    {"error":{"code":"upload_missing","message":"The file is not in storage. Upload it first.","request_id":"7e0312d6acff","fields":[]}}
  10. Téléversez le fichier directement vers le stockage, puis terminez :

    Fenêtre de terminal
    curl -s -o /dev/null -w '%{http_code}\n' -X PUT -H 'x-ms-blob-type: BlockBlob' \
    -H 'Content-Type: image/png' --data-binary @photo-1.png "$URL"
    curl -s -X POST http://127.0.0.1:8000/v1/attachments/$ID/complete
    201
    {"attachment_id":"34cf7dbf-8b5f-4b2b-ab87-1776f335a986","file_name":"photo-1.png","content_type":"image/png","size_bytes":3390,"status":"stored","uploaded_at":"2026-10-10T04:13:10.151351Z"}

    Dans PowerShell, les trois mêmes requêtes sont plus courtes, car Invoke-RestMethod lit le JSON pour vous (commandes écrites, mais non testées) :

    Fenêtre de terminal
    $r = Invoke-RestMethod -Method Post -Uri http://127.0.0.1:8000/v1/tickets/T-30002/attachments -ContentType 'application/json' -Body '{"file_name": "photo-1.png", "content_type": "image/png", "size_bytes": 3390}'
    Invoke-WebRequest -Method Put -Uri $r.upload_url -Headers @{ 'x-ms-blob-type' = 'BlockBlob' } -ContentType 'image/png' -InFile photo-1.png
    Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:8000/v1/attachments/$($r.attachment_id)/complete"
  11. Obtenez une URL de téléchargement et utilisez-la :

    Fenêtre de terminal
    curl -s http://127.0.0.1:8000/v1/attachments/$ID/download

    La réponse contient un url (URL) avec sp=r (lecture seule), valide pendant 5 minutes. Placez-le dans une variable, DL='…'. Puis curl -s -o /dev/null -w '%{http_code} %{size_download} bytes\n' "$DL" affiche 200 3390 bytes (200, 3 390 octets). Remplacez photo-1.png? par photo-2.png? dans l’URL et réessayez : 403.

  12. Testez la limite de taille de deux façons. Annoncez d’abord 20 000 000 octets : l’API répond immédiatement 413 avec file_too_large (fichier trop volumineux). Annoncez ensuite 1 000 octets pour un fichier nommé small.png. Placez son attachment_id (identifiant de pièce jointe) dans ID et son upload_url (URL de téléversement) dans URL, comme à l’étape 8. Envoyez-y 12 000 000 octets :

    Fenêtre de terminal
    head -c 12000000 /dev/zero > big.png
    curl -s -o /dev/null -w '%{http_code}\n' -X PUT -H 'x-ms-blob-type: BlockBlob' \
    -H 'Content-Type: image/png' --data-binary @big.png "$URL"

    Le stockage répond 201. Terminez ce téléversement : 413 et file_too_large. Dans psql, la ligne de small.png indique maintenant rejected (rejeté), et le fichier a disparu du stockage. Sur Windows, créez le fichier avec [IO.File]::WriteAllBytes("$PWD\big.png", (New-Object byte[] 12000000)). Supprimez big.png et photo-1.png quand vous avez terminé.

Les sorties de ce module proviennent d’un seul ordinateur, un Apple M4 avec macOS 26.6 et 16 Go de mémoire. Docker Desktop 4.81.0 disposait de 8 Go et de 10 processeurs. Il exécutait PostgreSQL 18.6 avec pgvector 0.8.7, et Azurite 3.37.0. Vos durées seront différentes; les nombres seront les mêmes. Le cours a testé chaque étape uniquement sur macOS (Apple silicon). Les commandes Windows et Linux ont été écrites, mais pas exécutées.

Erreur courante

Azurite refuse toutes les requêtes : la version de l'API n'est pas prise en charge

Symptôme : python -m scripts.load --reset ou le premier téléversement s’arrête avec azure.core.exceptions.HttpResponseError et ce message :

The API version 2026-10-06 is not supported by Azurite. Please upgrade Azurite to latest version and retry. If you are using Azurite in Visual Studio, please check you have installed latest Visual Studio patch. Azurite command line parameter "--skipApiVersionCheck" or Visual Studio Code configuration "Skip Api Version Check" can skip this error.

Cause : chaque requête de azure-storage-blob indique la version de l’API de stockage qu’elle utilise. La version 12.31.0 du paquet utilise 2026-10-06. Cette version est plus récente qu’Azurite 3.37.0. Par précaution, Azurite refuse les versions qu’il ne connaît pas. Le cours a reproduit le problème en démarrant Azurite sans l’option (un cas construit).

Diagnostic : vérifiez comment Azurite a été démarré : la ligne command: du service azurite dans compose.yaml, ou docker compose ps azurite.

Solution : démarrez Azurite avec --skipApiVersionCheck, comme le fait le fichier compose.yaml du projet. Cette option indique à Azurite de répondre quand même. C’est sûr pour les appels de ce projet : dépôt, lecture, taille, suppression, liste et URL signées. Les tests prouvent qu’ils fonctionnent. Si vous avez écrit votre propre fichier Compose, ajoutez l’option et exécutez de nouveau docker compose up -d azurite. L’autre solution est une version plus récente d’Azurite, lorsqu’une version prendra en charge celle de l’API.

Commencez un nouveau téléversement de photo-1.png pour T-30002, mais annoncez "size_bytes": 1000. Téléversez le vrai fichier, de 3 390 octets, et terminez. Avant d’exécuter ces étapes, prédisez le code d’état et le code d’erreur. Vérifiez ensuite la ligne dans psql.

Afficher la réponse

Le téléversement lui-même fonctionne (201), car le lien n’impose aucune condition de taille. complete (terminer) répond :

{"error":{"code":"size_mismatch","message":"The file has 3390 bytes, not 1000.","request_id":"b6b5cc8b1873","fields":[]}}

Le code d’état est 422. Le fichier fait moins de 10 Mio, donc ce n’est pas file_too_large. Ce n’est pas le fichier que le client a annoncé. L’API le supprime, et la ligne devient rejected. Un client qui veut réessayer commence un nouveau téléversement.

Quand utiliser le stockage objet, et ce qu’il coûte

Section intitulée « Quand utiliser le stockage objet, et ce qu’il coûte »

Omar pose la question du responsable de produit : « Combien cela nous coûte-t-il? » Le stockage objet est l’endroit le moins cher par gigaoctet. Les octets ne passent jamais par l’API ni par la base de données. Aucune des deux ne doit donc augmenter sa capacité pour eux. Utilisez-le pour tout fichier qu’une personne téléverse ou télécharge en entier : photos, reçus, PDF, exportations et fichiers d’un assistant documentaire.

Il demande tout de même du travail :

  • Deux endroits pour une pièce jointe. La ligne et le fichier peuvent diverger : une ligne dont le fichier manque, ou un fichier qu’aucune ligne ne référence. Les transactions de la base de données ne s’étendent pas au stockage objet. Restauration et conservation construit l’analyse qui repère les deux cas.
  • Les sauvegardes sont séparées. pg_dump ne copie pas les fichiers.
  • Un lien signé fonctionne pour toute personne qui le possède. Gardez sa durée de validité courte et ne l’enregistrez jamais dans les journaux.
  • Les petites données structurées n’y ont pas leur place. Le sujet d’un billet, son état ou le coût d’une exécution d’IA demandent des requêtes, des contraintes et des jointures. Ils restent dans PostgreSQL.

Une colonne bytea peut tout de même convenir à quelques petits fichiers essentiels. Utilisez-la quand un fichier doit partager la transaction et la sauvegarde de sa ligne. Une image de signature de quelques kilooctets en est un exemple.

Vérifiez votre compréhension

Pourquoi la ligne commence-t-elle en attente, et qu'est-ce qui la fait passer à l'état stocké? Quel problème surviendrait si l'API créait la ligne à l'état stocké dès le départ?

Afficher la réponse

Au départ, le fichier n’existe pas encore; le client l’a seulement annoncé. La ligne reste pending (en attente) jusqu’à complete. L’API demande ensuite au stockage la taille réelle, lit les octets et calcule le SHA-256. La ligne devient stored (stocké) seulement si la taille correspond à celle annoncée et ne dépasse pas 10 Mio. Si la ligne était stored dès le départ, le billet afficherait des fichiers jamais reçus ou d’une autre taille. Le CHECK (contrôle) qui exige une somme de contrôle et une heure de téléversement pour un fichier stocké empêche cela.

Vérifiez votre compréhension

Prédisez : un client partage un lien de téléchargement dans un forum public. Que peuvent faire les autres personnes avec ce lien, et pendant combien de temps?

Afficher la réponse

Elles peuvent télécharger ce seul fichier, et rien d’autre, jusqu’à l’heure indiquée dans se (expiration). Cela représente au maximum 5 minutes après la création du lien par l’API. Elles ne peuvent pas changer le nom pour obtenir un autre fichier, car la signature ne correspondrait pas (403). Elles ne peuvent pas téléverser avec ce lien, car sp=r permet seulement la lecture (403). Après l’expiration, le lien renvoie 403. Voilà pourquoi le projet utilise des liens de téléchargement de courte durée et en crée un nouveau pour chaque requête.

Vérifiez votre compréhension

Une SAS Azure ne peut pas limiter la taille d'un téléversement. Nommez les endroits où le projet impose la limite de 10 Mio, et la fonction S3 qui la vérifie pendant le téléversement.

Afficher la réponse

L’API vérifie la taille annoncée au début du téléversement (413 avant tout téléversement). Elle vérifie la taille réelle à complete : elle supprime un fichier plus gros, et la ligne devient rejected. La base de données contient un CHECK (size_bytes <= 10485760), donc aucun programme ne peut enregistrer un fichier plus gros. Le POST présigné d’Amazon S3 peut inclure une condition content-length-range (plage de taille du contenu), que S3 vérifie pendant le téléversement. Le cours la décrit, mais ne l’exécute pas.

Récapitulatif

  • Le stockage objet conserve les fichiers comme des objets avec une clé. PostgreSQL conserve une ligne par fichier : billet, nom, type, taille, somme de contrôle et object_key (clé d’objet).
  • Une URL signée autorise une action sur un objet jusqu’à une heure donnée. Un autre nom de fichier, la mauvaise action ou une tentative tardive ont chacun donné 403.
  • Le téléversement prend quatre requêtes : début (une ligne pending), PUT vers le stockage, complete (l’API vérifie le vrai fichier), téléchargement. L’API ne transporte jamais les octets et ne garde aucune connexion pendant les appels au stockage.
  • Une SAS Azure n’a aucune limite de taille : un téléversement de 12 Mo a obtenu 201. L’API vérifie donc à complete et supprime le fichier (413). Le POST présigné de S3 peut vérifier pendant le téléversement.
  • Les règles de cycle de vie suppriment ou déplacent les objets selon leur préfixe et leur âge, dans le nuage; Azurite ne les exécute pas.
  • Votre projet contient Azurite, la migration 009 et les points de terminaison des fichiers. python -m pytest affiche 95 passed, 1 deselected.

Leçon suivante : Stockage des vecteurs