Les fichiers téléchargés sur Amazon S3 d'une taille inférieure à 5 Go ont un ETag qui est simplement le hachage MD5 du fichier, ce qui permet de vérifier facilement si vos fichiers locaux sont identiques à ceux que vous avez placés sur S3.
Mais si votre fichier dépasse 5 Go, Amazon calcule l’ETag différemment.
Par exemple, j'ai effectué un téléchargement en plusieurs parties d'un fichier de 5 970 150 664 octets en 380 parties. Maintenant, S3 montre qu’il a un ETag de 6bcf86bed8807b8e78f0fc6e0a53079d-380
. Mon fichier local a un hachage md5 de 702242d3703818ddefe6bf7da2bed757
. Je pense que le nombre après le tiret est le nombre de parties dans le téléchargement en plusieurs parties.
Je soupçonne également que le nouvel ETag (avant le tiret) est toujours un hachage MD5, mais avec quelques métadonnées incluses tout au long du processus de téléchargement en plusieurs parties.
Est-ce que quelqu'un sait comment calculer l'ETag en utilisant le même algorithme que Amazon S3?
Je viens de vérifier un. Chapeau à Amazon pour avoir rendu les choses assez simples pour être devinables.
Supposons que vous ayez téléchargé un fichier de 14 Mo et que la taille de votre pièce soit de 5 Mo. Calculez 3 sommes de contrôle MD5 correspondant à chaque partie, c’est-à-dire la somme de contrôle des 5 premiers Mo, des 5 derniers Mo et des 4 derniers Mo. Ensuite, prenez la somme de contrôle de leur concaténation. Les sommes de contrôle MD5 étant des représentations hexadécimales de données binaires, veillez simplement à prendre le MD5 de la concaténation binaire décodée, et non de la concaténation codée ASCII ou UTF-8. Lorsque cela est fait, ajoutez un trait d'union et le nombre de parties pour obtenir l'ETag.
Voici les commandes pour le faire sur Mac OS X à partir de la console:
$ dd bs=1m count=5 skip=0 if=someFile | md5 >>checksums.txt
5+0 records in
5+0 records out
5242880 bytes transferred in 0.019611 secs (267345449 bytes/sec)
$ dd bs=1m count=5 skip=5 if=someFile | md5 >>checksums.txt
5+0 records in
5+0 records out
5242880 bytes transferred in 0.019182 secs (273323380 bytes/sec)
$ dd bs=1m count=5 skip=10 if=someFile | md5 >>checksums.txt
2+1 records in
2+1 records out
2599812 bytes transferred in 0.011112 secs (233964895 bytes/sec)
À ce stade, toutes les sommes de contrôle sont dans checksums.txt
. Pour les concaténer, décoder l'hex et obtenir la somme de contrôle MD5 du lot, utilisez simplement
$ xxd -r -p checksums.txt | md5
Et maintenant, ajoutez "-3" pour obtenir l’ETag, car il y avait 3 parties.
Il convient de noter que md5
sous Mac OS X écrit simplement la somme de contrôle, mais que md5sum
sous Linux génère également le nom du fichier. Vous aurez besoin de supprimer cela, mais je suis sûr qu'il existe une option pour ne produire que les sommes de contrôle. Vous n'avez pas à vous soucier des espaces, car xxd
l'ignorera.
Note: Si vous avez chargé avec aws-cli via aws s3 cp
, vous avez probablement une taille de morceau de 8 Mo. Selon le docs , il s'agit de la valeur par défaut.
Mise à jour: On m'a parlé d'une implémentation de ceci à https://github.com/Teachnova/s3md5 , qui ne fonctionne pas sous OS X. Voici un résumé que j'ai écrit avec un script de travail pour OS X .
Même algorithme, version Java: (BaseEncoding, Hasher, Hashing, etc. provient de la bibliothèque guava
/**
* Generate checksum for object came from multipart upload</p>
* </p>
* AWS S3 spec: Entity tag that identifies the newly created object's data. Objects with different object data will have different entity tags. The entity tag is an opaque string. The entity tag may or may not be an MD5 digest of the object data. If the entity tag is not an MD5 digest of the object data, it will contain one or more nonhexadecimal characters and/or will consist of less than 32 or more than 32 hexadecimal digits.</p>
* Algorithm follows AWS S3 implementation: https://github.com/Teachnova/s3md5</p>
*/
private static String calculateChecksumForMultipartUpload(List<String> md5s) {
StringBuilder stringBuilder = new StringBuilder();
for (String md5:md5s) {
stringBuilder.append(md5);
}
String hex = stringBuilder.toString();
byte raw[] = BaseEncoding.base16().decode(hex.toUpperCase());
Hasher hasher = Hashing.md5().newHasher();
hasher.putBytes(raw);
String digest = hasher.hash().toString();
return digest + "-" + md5s.size();
}
L'algorithme est littéralement (copié du fichier Lisez-moi dans l'implémentation python):
Je ne sais pas si cela peut aider:
Nous sommes en train de faire un bidouillage horrible (mais jusqu'ici utile) pour corriger ces mauvais ETags dans des fichiers téléchargés en plusieurs parties, qui consiste à appliquer une modification au fichier dans le compartiment; cela déclenche un recalcul md5 à partir d'Amazon qui modifie l'ETag pour qu'il corresponde à la signature md5 réelle.
Dans notre cas:
Fichier: bucket/Foo.mpg.gpg
Nous ne connaissons pas l'algorithme, mais comme nous pouvons "réparer" l'ETag, nous n'avons pas besoin de nous en préoccuper.
Sur la base des réponses fournies ici, j’ai écrit une implémentation Python qui calcule correctement les ETags de fichiers en plusieurs parties et en une partie.
def calculate_s3_etag(file_path, chunk_size=8 * 1024 * 1024):
md5s = []
with open(file_path, 'rb') as fp:
while True:
data = fp.read(chunk_size)
if not data:
break
md5s.append(hashlib.md5(data))
if len(md5s) == 1:
return '"{}"'.format(md5s[0].hexdigest())
digests = b''.join(m.digest() for m in md5s)
digests_md5 = hashlib.md5(digests)
return '"{}-{}"'.format(digests_md5.hexdigest(), len(md5s))
La taille de bloc par défaut est 8 Mo, elle est utilisée par l'outil aws cli
officiel. Elle permet le téléchargement en plusieurs parties pour plus de 2 morceaux. Cela devrait fonctionner sous Python 2 et 3.
Dans une réponse ci-dessus, quelqu'un a demandé s'il existait un moyen d'obtenir le md5 pour les fichiers de taille supérieure à 5G.
Une réponse que je pourrais donner pour obtenir la valeur MD5 (pour les fichiers de taille supérieure à 5G) serait soit de l’ajouter manuellement aux métadonnées, soit d’utiliser un programme permettant d’effectuer vos téléchargements et d’ajouter les informations.
Par exemple, j'ai utilisé s3cmd pour télécharger un fichier et les métadonnées suivantes ont été ajoutées.
$ aws s3api head-object --bucket xxxxxxx --key noarch/epel-release-6-8.noarch.rpm
{
"AcceptRanges": "bytes",
"ContentType": "binary/octet-stream",
"LastModified": "Sat, 19 Sep 2015 03:27:25 GMT",
"ContentLength": 14540,
"ETag": "\"2cd0ae668a585a14e07c2ea4f264d79b\"",
"Metadata": {
"s3cmd-attrs": "uid:502/gname:staff/uname:xxxxxx/gid:20/mode:33188/mtime:1352129496/atime:1441758431/md5:2cd0ae668a585a14e07c2ea4f264d79b/ctime:1441385182"
}
}
L'ETag n'est pas une solution directe, mais un moyen de renseigner les métadonnées souhaitées (MD5) de manière à ce que vous puissiez y accéder. Il échouera quand même si quelqu'un télécharge le fichier sans métadonnées.
Selon la documentation AWS, ETag n'est pas un hachage MD5 pour un téléchargement en plusieurs parties ni pour un objet chiffré: http://docs.aws.Amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html
Les objets créés par l'objet PUT, l'objet POST, ou l'opération de copie, ou via AWS Management Console, et chiffrés par SSE-S3 ou par un texte en clair, ont des balises ETags qui sont un condensé MD5 de leurs données d'objet.
Les objets créés par l'objet PUT, l'objet POST, ou l'opération de copie, ou via AWS Management Console, et chiffrés par SSE-C ou SSE-KMS, ont des balises ETags qui ne sont pas un résumé MD5 de leurs données d'objet.
Si un objet est créé à l'aide de l'opération de téléchargement en plusieurs parties ou de la copie partielle, l'ETag n'est pas un résumé MD5, quelle que soit la méthode de cryptage.
Et voici une version PHP du calcul de l'ETag:
function calculate_aws_etag($filename, $chunksize) {
/*
DESCRIPTION:
- calculate Amazon AWS ETag used on the S3 service
INPUT:
- $filename : path to file to check
- $chunksize : chunk size in Megabytes
OUTPUT:
- ETag (string)
*/
$chunkbytes = $chunksize*1024*1024;
if (filesize($filename) < $chunkbytes) {
return md5_file($filename);
} else {
$md5s = array();
$handle = fopen($filename, 'rb');
if ($handle === false) {
return false;
}
while (!feof($handle)) {
$buffer = fread($handle, $chunkbytes);
$md5s[] = md5($buffer);
unset($buffer);
}
fclose($handle);
$concat = '';
foreach ($md5s as $indx => $md5) {
$concat .= hex2bin($md5);
}
return md5($concat) .'-'. count($md5s);
}
}
$etag = calculate_aws_etag('path/to/myfile.ext', 8);
Et voici une version améliorée qui peut être vérifiée par rapport à un ETag attendu - et même deviner la taille de morceau si vous ne le connaissez pas!
function calculate_etag($filename, $chunksize, $expected = false) {
/*
DESCRIPTION:
- calculate Amazon AWS ETag used on the S3 service
INPUT:
- $filename : path to file to check
- $chunksize : chunk size in Megabytes
- $expected : verify calculated etag against this specified etag and return true or false instead
- if you make chunksize negative (eg. -8 instead of 8) the function will guess the chunksize by checking all possible sizes given the number of parts mentioned in $expected
OUTPUT:
- ETag (string)
- or boolean true|false if $expected is set
*/
if ($chunksize < 0) {
$do_guess = true;
$chunksize = 0 - $chunksize;
} else {
$do_guess = false;
}
$chunkbytes = $chunksize*1024*1024;
$filesize = filesize($filename);
if ($filesize < $chunkbytes && (!$expected || !preg_match("/^\\w{32}-\\w+$/", $expected))) {
$return = md5_file($filename);
if ($expected) {
$expected = strtolower($expected);
return ($expected === $return ? true : false);
} else {
return $return;
}
} else {
$md5s = array();
$handle = fopen($filename, 'rb');
if ($handle === false) {
return false;
}
while (!feof($handle)) {
$buffer = fread($handle, $chunkbytes);
$md5s[] = md5($buffer);
unset($buffer);
}
fclose($handle);
$concat = '';
foreach ($md5s as $indx => $md5) {
$concat .= hex2bin($md5);
}
$return = md5($concat) .'-'. count($md5s);
if ($expected) {
$expected = strtolower($expected);
$matches = ($expected === $return ? true : false);
if ($matches || $do_guess == false || strlen($expected) == 32) {
return $matches;
} else {
// Guess the chunk size
preg_match("/-(\\d+)$/", $expected, $match);
$parts = $match[1];
$min_chunk = ceil($filesize / $parts /1024/1024);
$max_chunk = floor($filesize / ($parts-1) /1024/1024);
$found_match = false;
for ($i = $min_chunk; $i <= $max_chunk; $i++) {
if (calculate_aws_etag($filename, $i) === $expected) {
$found_match = true;
break;
}
}
return $found_match;
}
} else {
return $return;
}
}
}
Voici l'algorithme en Ruby ...
require 'digest'
# PART_SIZE should match the chosen part size of the multipart upload
# Set here as 10MB
PART_SIZE = 1024*1024*10
class File
def each_part(part_size = PART_SIZE)
yield read(part_size) until eof?
end
end
file = File.new('<path_to_file>')
hashes = []
file.each_part do |part|
hashes << Digest::MD5.hexdigest(part)
end
multipart_hash = Digest::MD5.hexdigest([hashes.join].pack('H*'))
multipart_etag = "#{multipart_hash}-#{hashes.count}"
Merci à Hex2Bin le plus court de Ruby et Envois multipart sur S3 ...
node.js implementation -
const fs = require('fs');
const crypto = require('crypto');
const chunk = 1024 * 1024 * 5; // 5MB
const md5 = data => crypto.createHash('md5').update(data).digest('hex');
const getEtagOfFile = (filePath) => {
const stream = fs.readFileSync(filePath);
if (stream.length < chunk) {
return md5(stream);
}
const md5Chunks = [];
const chunksNumber = Math.ceil(stream.length / chunk);
for (let i = 0; i < chunksNumber; i++) {
const chunkStream = stream.slice(i * chunk, (i + 1) * chunk);
md5Chunks.Push(md5(chunkStream));
}
return `${md5(Buffer.from(md5Chunks.join(''), 'hex'))}-${chunksNumber}`;
};
L'algorithme présenté dans cette réponse est précis. c’est-à-dire que vous prenez le condensé binaire md5 de 128 bits de chaque partie, les concaténer dans un document et le hacher.
L’algorithme mérite également d’être souligné: si vous copiez ou faites une copie sur place de votre objet téléchargé en plusieurs parties terminé (aka PUT-COPY), S3 recalculera l’ETAG et utilisera la version simple de l’algorithme. c'est-à-dire que l'objet de destination aura un etag sans le trait d'union.
Vous avez probablement déjà envisagé cela, mais si vos fichiers font moins de 5 Go et que vous connaissez déjà leur MD5, la parallélisation du téléchargement n'apporte que peu ou pas d'avantages (par exemple, vous transférez le téléchargement à partir d'un réseau lent ou le téléchargement à partir d'un disque lent. ), vous pouvez également envisager d’utiliser un simple PUT au lieu d’une option multipart et de transmettre votre Content-MD5 connu dans les en-têtes de votre demande - Amazon échouera le téléchargement s’ils ne correspondent pas. Gardez à l'esprit que vous êtes facturé pour chaque UploadPart.
De plus, chez certains clients, le passage d'un MD5 connu pour la saisie d'une opération PUT évitera au client de recalculer le MD5 pendant le transfert. Dans boto3 (python), vous utiliseriez le paramètre ContentMD5
de la méthode client.put_object () , par exemple. Si vous omettez le paramètre et que vous connaissez déjà le MD5, le client gaspille des cycles en le calculant à nouveau avant le transfert.
J'ai une solution pour iOS et macOS sans l'aide d'auxiliaires externes tels que dd et xxd. Je viens de le trouver, donc je le rapporte tel quel, avec l'intention de l'améliorer ultérieurement. Pour le moment, il s'appuie à la fois sur les codes Objective-C et Swift. Tout d’abord, créez cette classe d’aide dans Objective-C:
AWS3MD5Hash.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface AWS3MD5Hash : NSObject
- (NSData *)dataFromFile:(FILE *)theFile startingOnByte:(UInt64)startByte length:(UInt64)length filePath:(NSString *)path singlePartSize:(NSUInteger)partSizeInMb;
- (NSData *)dataFromBigData:(NSData *)theData startingOnByte:(UInt64)startByte length:(UInt64)length;
- (NSData *)dataFromHexString:(NSString *)sourceString;
@end
NS_ASSUME_NONNULL_END
AWS3MD5Hash.m
#import "AWS3MD5Hash.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SIZE 256
@implementation AWS3MD5Hash
- (NSData *)dataFromFile:(FILE *)theFile startingOnByte:(UInt64)startByte length:(UInt64)length filePath:(NSString *)path singlePartSize:(NSUInteger)partSizeInMb {
char *buffer = malloc(length);
NSURL *fileURL = [NSURL fileURLWithPath:path];
NSNumber *fileSizeValue = nil;
NSError *fileSizeError = nil;
[fileURL getResourceValue:&fileSizeValue
forKey:NSURLFileSizeKey
error:&fileSizeError];
NSInteger __unused result = fseek(theFile,startByte,SEEK_SET);
if (result != 0) {
free(buffer);
return nil;
}
NSInteger result2 = fread(buffer, length, 1, theFile);
NSUInteger difference = fileSizeValue.integerValue - startByte;
NSData *toReturn;
if (result2 == 0) {
toReturn = [NSData dataWithBytes:buffer length:difference];
} else {
toReturn = [NSData dataWithBytes:buffer length:result2 * length];
}
free(buffer);
return toReturn;
}
- (NSData *)dataFromBigData:(NSData *)theData startingOnByte: (UInt64)startByte length:(UInt64)length {
NSUInteger fileSizeValue = theData.length;
NSData *subData;
if (startByte + length > fileSizeValue) {
subData = [theData subdataWithRange:NSMakeRange(startByte, fileSizeValue - startByte)];
} else {
subData = [theData subdataWithRange:NSMakeRange(startByte, length)];
}
return subData;
}
- (NSData *)dataFromHexString:(NSString *)string {
string = [string lowercaseString];
NSMutableData *data= [NSMutableData new];
unsigned char whole_byte;
char byte_chars[3] = {'\0','\0','\0'};
NSInteger i = 0;
NSInteger length = string.length;
while (i < length-1) {
char c = [string characterAtIndex:i++];
if (c < '0' || (c > '9' && c < 'a') || c > 'f')
continue;
byte_chars[0] = c;
byte_chars[1] = [string characterAtIndex:i++];
whole_byte = strtol(byte_chars, NULL, 16);
[data appendBytes:&whole_byte length:1];
}
return data;
}
@end
Maintenant, créez un fichier Swift simple:
AWS Extensions.Swift
import UIKit
import CommonCrypto
extension URL {
func calculateAWSS3MD5Hash(_ numberOfParts: UInt64) -> String? {
do {
var fileSize: UInt64!
var calculatedPartSize: UInt64!
let attr:NSDictionary? = try FileManager.default.attributesOfItem(atPath: self.path) as NSDictionary
if let _attr = attr {
fileSize = _attr.fileSize();
if numberOfParts != 0 {
let partSize = Double(fileSize / numberOfParts)
var partSizeInMegabytes = Double(partSize / (1024.0 * 1024.0))
partSizeInMegabytes = ceil(partSizeInMegabytes)
calculatedPartSize = UInt64(partSizeInMegabytes)
if calculatedPartSize % 2 != 0 {
calculatedPartSize += 1
}
if numberOfParts == 2 || numberOfParts == 3 { // Very important when there are 2 or 3 parts, in the majority of times
// the calculatedPartSize is already 8. In the remaining cases we force it.
calculatedPartSize = 8
}
if mainLogToggling {
print("The calculated part size is \(calculatedPartSize!) Megabytes")
}
}
}
if numberOfParts == 0 {
let string = self.memoryFriendlyMd5Hash()
return string
}
let hasher = AWS3MD5Hash.init()
let file = fopen(self.path, "r")
defer { let result = fclose(file)}
var index: UInt64 = 0
var bigString: String! = ""
var data: Data!
while autoreleasepool(invoking: {
if index == (numberOfParts-1) {
if mainLogToggling {
//print("Siamo all'ultima linea.")
}
}
data = hasher.data(from: file!, startingOnByte: index * calculatedPartSize * 1024 * 1024, length: calculatedPartSize * 1024 * 1024, filePath: self.path, singlePartSize: UInt(calculatedPartSize))
bigString = bigString + MD5.get(data: data) + "\n"
index += 1
if index == numberOfParts {
return false
}
return true
}) {}
let final = MD5.get(data :hasher.data(fromHexString: bigString)) + "-\(numberOfParts)"
return final
} catch {
}
return nil
}
func memoryFriendlyMd5Hash() -> String? {
let bufferSize = 1024 * 1024
do {
// Open file for reading:
let file = try FileHandle(forReadingFrom: self)
defer {
file.closeFile()
}
// Create and initialize MD5 context:
var context = CC_MD5_CTX()
CC_MD5_Init(&context)
// Read up to `bufferSize` bytes, until EOF is reached, and update MD5 context:
while autoreleasepool(invoking: {
let data = file.readData(ofLength: bufferSize)
if data.count > 0 {
data.withUnsafeBytes {
_ = CC_MD5_Update(&context, $0, numericCast(data.count))
}
return true // Continue
} else {
return false // End of file
}
}) { }
// Compute the MD5 digest:
var digest = Data(count: Int(CC_MD5_DIGEST_LENGTH))
digest.withUnsafeMutableBytes {
_ = CC_MD5_Final($0, &context)
}
let hexDigest = digest.map { String(format: "%02hhx", $0) }.joined()
return hexDigest
} catch {
print("Cannot open file:", error.localizedDescription)
return nil
}
}
struct MD5 {
static func get(data: Data) -> String {
var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
let _ = data.withUnsafeBytes { bytes in
CC_MD5(bytes, CC_LONG(data.count), &digest)
}
var digestHex = ""
for index in 0..<Int(CC_MD5_DIGEST_LENGTH) {
digestHex += String(format: "%02x", digest[index])
}
return digestHex
}
// The following is a memory friendly version
static func get2(data: Data) -> String {
var currentIndex = 0
let bufferSize = 1024 * 1024
//var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
// Create and initialize MD5 context:
var context = CC_MD5_CTX()
CC_MD5_Init(&context)
while autoreleasepool(invoking: {
var subData: Data!
if (currentIndex + bufferSize) < data.count {
subData = data.subdata(in: Range.init(NSMakeRange(currentIndex, bufferSize))!)
currentIndex = currentIndex + bufferSize
} else {
subData = data.subdata(in: Range.init(NSMakeRange(currentIndex, data.count - currentIndex))!)
currentIndex = currentIndex + (data.count - currentIndex)
}
if subData.count > 0 {
subData.withUnsafeBytes {
_ = CC_MD5_Update(&context, $0, numericCast(subData.count))
}
return true
} else {
return false
}
}) { }
// Compute the MD5 digest:
var digest = Data(count: Int(CC_MD5_DIGEST_LENGTH))
digest.withUnsafeMutableBytes {
_ = CC_MD5_Final($0, &context)
}
var digestHex = ""
for index in 0..<Int(CC_MD5_DIGEST_LENGTH) {
digestHex += String(format: "%02x", digest[index])
}
return digestHex
}
}
Maintenant, ajoutez:
#import "AWS3MD5Hash.h"
à votre en-tête Objective-C Bridging. Vous devriez être d'accord avec cette configuration.
Exemple d'utilisation
Pour tester cette configuration, vous pouvez appeler la méthode suivante dans l'objet en charge de la gestion des connexions AWS:
func getMd5HashForFile() {
let credentialProvider = AWSCognitoCredentialsProvider(regionType: AWSRegionType.USEast2, identityPoolId: "<INSERT_POOL_ID>")
let configuration = AWSServiceConfiguration(region: AWSRegionType.APSoutheast2, credentialsProvider: credentialProvider)
configuration?.timeoutIntervalForRequest = 3.0
configuration?.timeoutIntervalForResource = 3.0
AWSServiceManager.default().defaultServiceConfiguration = configuration
AWSS3.register(with: configuration!, forKey: "defaultKey")
let s3 = AWSS3.s3(forKey: "defaultKey")
let headObjectRequest = AWSS3HeadObjectRequest()!
headObjectRequest.bucket = "<NAME_OF_YOUR_BUCKET>"
headObjectRequest.key = self.latestMapOnServer.key
let _: AWSTask? = s3.headObject(headObjectRequest).continueOnSuccessWith { (awstask) -> Any? in
let headObjectOutput: AWSS3HeadObjectOutput? = awstask.result
var ETag = headObjectOutput?.eTag!
// Here you should parse the returned Etag and extract the number of parts to provide to the helper function. Etags end with a "-" followed by the number of parts. If you don't see this format, then pass 0 as the number of parts.
ETag = ETag!.replacingOccurrences(of: "\"", with: "")
print("headObjectOutput.ETag \(ETag!)")
let mapOnDiskUrl = self.getMapsDirectory().appendingPathComponent(self.latestMapOnDisk!)
let hash = mapOnDiskUrl.calculateAWSS3MD5Hash(<Take the number of parts from the ETag returned by the server>)
if hash == ETag {
print("They are the same.")
}
print ("\(hash!)")
return nil
}
}
Si l'ETag renvoyé par le serveur ne comporte pas "-" à la fin de l'ETag, il suffit de passer 0 pour calculer AWSS3MD5Hash. Veuillez commenter si vous rencontrez des problèmes. Je travaille sur une seule solution Swift, je mettrai à jour cette réponse dès que j'aurai terminé. Merci