{message}
-{details}
- {stack && ( -
- {stack}
-
- )}
- diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f77b335 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +.gitignore +README.md +Dockerfile +.dockerignore diff --git a/.github/workflows/ci-cd.yaml b/.github/workflows/ci-cd.yaml new file mode 100644 index 0000000..92e6ab2 --- /dev/null +++ b/.github/workflows/ci-cd.yaml @@ -0,0 +1,129 @@ +name: CI/CD Pipeline + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.21" + + - name: Test Backend + working-directory: ./app/back + continue-on-error: true + run: | + go mod tidy + go mod download + go test -v ./... + + - name: Test CDN + working-directory: ./app/cdn + continue-on-error: true + run: | + go mod tidy + go mod download + go test -v ./... + + build: + needs: test + if: always() + runs-on: ubuntu-latest + strategy: + matrix: + component: [back, front, cdn] + steps: + - uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push ${{ matrix.component }} + uses: docker/build-push-action@v5 + with: + context: . + file: docker/${{ matrix.component }}/Dockerfile + push: ${{ github.ref == 'refs/heads/main' }} + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/goofy-${{ matrix.component }}:latest + ${{ secrets.DOCKERHUB_USERNAME }}/goofy-${{ matrix.component }}:${{ github.sha }} + cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/goofy-${{ matrix.component }}:buildcache + cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/goofy-${{ matrix.component }}:buildcache,mode=max + + deploy: + needs: build + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: eu-west-3 + + - name: Update kube config + run: aws eks update-kubeconfig --name hetic-groupe-3 + + - name: Create or update Kubernetes secrets + run: | + # Créer ou mettre à jour les secrets de l'application + kubectl create secret generic app-secrets \ + --namespace goofy-cdn \ + --from-literal=mongodb-uri="${{ secrets.MONGODB_URI }}" \ + --from-literal=jwt-secret="${{ secrets.JWT_SECRET }}" \ + --dry-run=client -o yaml | kubectl apply -f - + + # Créer ou mettre à jour les secrets du monitoring + kubectl create secret generic monitoring-secrets \ + --namespace monitoring \ + --from-literal=grafana-admin-password="${{ secrets.GRAFANA_ADMIN_PASSWORD }}" \ + --dry-run=client -o yaml | kubectl apply -f - + + - name: Deploy to Kubernetes + run: | + # Mise à jour des images dans les deployments + kubectl set image deployment/backend backend=\ + ${{ secrets.DOCKERHUB_USERNAME }}/goofy-back:${{ github.sha }} -n goofy-cdn + kubectl set image deployment/frontend frontend=\ + ${{ secrets.DOCKERHUB_USERNAME }}/goofy-front:${{ github.sha }} -n goofy-cdn + kubectl set image deployment/cdn cdn=\ + ${{ secrets.DOCKERHUB_USERNAME }}/goofy-cdn:${{ github.sha }} -n goofy-cdn + + # Vérification du déploiement + kubectl rollout status deployment/backend -n goofy-cdn + kubectl rollout status deployment/frontend -n goofy-cdn + kubectl rollout status deployment/cdn -n goofy-cdn + + # - name: Notify Slack on Success + # if: success() + # uses: rtCamp/action-slack-notify@v2 + # env: + # SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + # SLACK_MESSAGE: "Déploiement réussi sur production :rocket:" + # SLACK_COLOR: good + + # - name: Notify Slack on Failure + # if: failure() + # uses: rtCamp/action-slack-notify@v2 + # env: + # SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + # SLACK_MESSAGE: ":x: Échec du déploiement sur production" + # SLACK_COLOR: danger diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d1139f --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.idea/ +.windsurfrules + +# Temporary files +**/**/tmp +tmp/ +*.log +air.log + +# Binary files +**/main + +# Environment variables +.env +.env.localgit +uploads \ No newline at end of file diff --git a/README.md b/README.md index 645b8bc..1fbe301 100644 --- a/README.md +++ b/README.md @@ -1,115 +1,390 @@ -# CDN Go - Projet de Content Delivery Network +# CDN Go – Projet de Réseau de Diffusion de Contenu -Ce projet implémente un Content Delivery Network (CDN) en Go, conçu pour optimiser la distribution de contenu web avec des fonctionnalités avancées de mise en cache, de répartition de charge et de monitoring. +Ce projet, développé en Go, met en place un Content Delivery Network (CDN) afin d’optimiser la distribution de contenu web. Il intègre des mécanismes avancés de mise en cache, de répartition de charge et de monitoring. ## 🚀 Fonctionnalités - **Proxy HTTP** : Redirection intelligente des requêtes -- **Système de Cache** : +- **Mécanisme de Cache** : - Cache LRU en mémoire - - Support Redis pour le cache distribué -- **Load Balancing** : + - Intégration de Redis pour un cache distribué +- **Répartition de Charge** : - Round Robin - Weighted Round Robin - Least Connections - **Sécurité** : - - Rate Limiting - - Protection DDoS - - Headers de sécurité HTTP + - Limitation du débit (Rate Limiting) + - Protection contre les attaques DDoS + - Application de headers de sécurité HTTP - **Monitoring** : - - Métriques Prometheus - - Visualisation Grafana - - Logging structuré avec Logrus + - Collecte de métriques via Prometheus + - Visualisation avec Grafana + - Logging structuré grâce à Logrus ## 🛠 Prérequis - Docker - Docker Compose -- Go 1.23+ (pour le développement local) +- Go 1.23 ou supérieur (pour le développement local) ## 🚦 Démarrage -1. **Mode Développement** : +### 1. Mode Développement + +Lancer l’application en mode développement avec hot-reload : + ```bash -docker compose -f docker-compose.dev.yml up -d +docker compose up app-dev ``` -- Hot-reload activé -- Accessible sur http://localhost:8080 -- Métriques sur http://localhost:8080/metrics -2. **Mode Production** : +- Accessible via [http://localhost:8080](http://localhost:8080) +- Les métriques sont disponibles sur [http://localhost:8080/metrics](http://localhost:8080/metrics) + +### 2. Mode Production + +Démarrer en mode production : + ```bash -docker compose -f docker-compose.prod.yml up -d +docker compose up app-prod ``` -- Optimisé pour la production -- Accessible sur http://localhost:8081 -- Métriques sur http://localhost:8081/metrics -3. **Services additionnels** : -- Grafana : http://localhost:3000 (admin/admin) -- Prometheus : http://localhost:9090 -- Redis : localhost:6379 +- Optimisé pour un environnement de production +- Accessible via [http://localhost:8081](http://localhost:8081) +- Les métriques se trouvent sur [http://localhost:8081/metrics](http://localhost:8081/metrics) + +### 3. Services Complémentaires + +- **Grafana** : [http://localhost:3000](http://localhost:3000) (identifiants par défaut : admin/admin) +- **Prometheus** : [http://localhost:9090](http://localhost:9090) +- **Redis** : Accessible sur localhost:6379 -## 🏗 Structure du Projet +## 🏗 Organisation du Projet ``` app/ ├── internal/ -│ ├── cache/ # Implémentation du cache (LRU, Redis) -│ ├── loadbalancer/ # Algorithmes de load balancing -│ └── middleware/ # Middlewares (sécurité, métriques) +│ ├── cache/ # Gestion du cache (implémentation LRU et intégration Redis) +│ ├── loadbalancer/ # Algorithmes de répartition de charge +│ └── middleware/ # Middlewares pour la sécurité et le monitoring ├── pkg/ -│ └── config/ # Configuration de l'application -└── main.go # Point d'entrée de l'application +│ └── config/ # Fichiers de configuration de l’application +└── main.go # Point d’entrée de l’application ``` -## 🔍 Fonctionnement Détaillé +## 🔍 Fonctionnement en Détail ### 1. Système de Cache + - **Cache LRU** (`internal/cache/cache.go`) : - - Implémente l'interface `Cache` - - Utilise `hashicorp/golang-lru` pour la gestion du cache en mémoire - - Limite configurable de la taille du cache + - Respecte l’interface `Cache` + - S’appuie sur la librairie `hashicorp/golang-lru` pour la gestion en mémoire + - Taille du cache configurable + - Cible uniquement les requêtes GET + - Durée de vie (TTL) des entrées paramétrable + +- **Endpoints de Gestion du Cache** : + - `POST /cache/purge` : Permet de vider l’intégralité du cache + Exemple d’utilisation : + ```bash + curl -X POST http://localhost:8080/cache/purge + ``` ### 2. Load Balancer -- **Implémentations** (`internal/loadbalancer/loadbalancer.go`) : - - `RoundRobin` : Distribution cyclique des requêtes - - `WeightedRoundRobin` : Distribution pondérée selon la capacité des serveurs - - `LeastConnections` : Envoi vers le serveur le moins chargé -### 3. Middlewares -- **Sécurité** (`internal/middleware/middleware.go`) : - - Rate limiting avec `golang.org/x/time/rate` - - Headers de sécurité HTTP - - Protection contre les attaques courantes +- **Implémentations** (voir `internal/loadbalancer/loadbalancer.go`) : + - **RoundRobin** : Distribution cyclique des requêtes + - **WeightedRoundRobin** : Distribution pondérée en fonction des capacités des serveurs + - **LeastConnections** : Acheminement vers le serveur avec le moins de connexions actives + +### 3. Endpoints API + +#### Backend Service (port 8080) + +- **Authentification** : + - `POST /register` : Inscription d’un nouvel utilisateur + - `POST /login` : Connexion d’un utilisateur + +- **Gestion des Fichiers** *(authentification requise)* : + - `POST /api/files` : Upload d’un fichier + - `GET /api/files/:id` : Récupération d’un fichier + - `DELETE /api/files/:id` : Suppression d’un fichier + +- **Gestion des Dossiers** *(authentification requise)* : + - `POST /api/folders` : Création d’un dossier + - `GET /api/folders/:id` : Affichage du contenu d’un dossier + - `DELETE /api/folders/:id` : Suppression d’un dossier + +- **Health Check** : + - `GET /health` : Vérification de l’état du service + +#### CDN Service (port 8080) + +- **Cache** : + - `POST /cache/purge` : Effacement du cache + - *Note* : Seules les requêtes GET sont mises en cache + +- **Monitoring** : + - `GET /metrics` : Exposition des métriques Prometheus + - `GET /health` : État de santé du CDN + - `GET /ready` : Vérification de la disponibilité ### 4. Monitoring -- **Métriques** : + +- **Métriques Collectées** : - Temps de réponse des requêtes - Nombre de requêtes par endpoint - - Taux de succès/erreur + - Taux de réussite vs. échec - Utilisation du cache +- **Visualisation** : Les données sont exploitées dans Grafana via Prometheus + ### 5. Application Principale -Le fichier `main.go` orchestre tous ces composants : -1. Initialise le logger et le cache -2. Configure le load balancer -3. Met en place les middlewares de sécurité et monitoring -4. Démarre le serveur HTTP avec gestion gracieuse de l'arrêt + +Le fichier `main.go` orchestre l’ensemble des composants en : +1. Initialisant le logger et le cache +2. Configurant le load balancer +3. Déployant les middlewares pour la sécurité et le monitoring +4. Démarrant le serveur HTTP avec une gestion gracieuse de l’arrêt ## 📊 Monitoring -### Métriques disponibles : -- `http_duration_seconds` : Temps de réponse des requêtes -- `http_requests_total` : Nombre total de requêtes par endpoint -- Visualisation dans Grafana via Prometheus +### Métriques Disponibles : + +- `http_duration_seconds` : Mesure du temps de réponse des requêtes +- `http_requests_total` : Compte total des requêtes par endpoint + +Les visualisations se font via Grafana, en s’appuyant sur Prometheus. ## 🔒 Sécurité -- Rate limiting : 100 requêtes/seconde par défaut -- Headers de sécurité : +- **Rate Limiting** : Limitation par défaut à 100 requêtes par seconde +- **Headers de Sécurité** : - `X-Frame-Options` - `X-Content-Type-Options` - `X-XSS-Protection` - `Content-Security-Policy` - - `Strict-Transport-Security` \ No newline at end of file + - `Strict-Transport-Security` + +## 🤝 Contribution + +Pour contribuer : + +1. Forkez le projet +2. Créez votre branche de travail (par exemple : `git checkout -b feature/amazing-feature`) +3. Effectuez vos commits (`git commit -m 'Ajout d’une fonctionnalité géniale'`) +4. Poussez votre branche (`git push origin feature/amazing-feature`) +5. Ouvrez une Pull Request + +## 🚀 Déploiement sur AWS EKS + +### Prérequis AWS + +- Un compte AWS avec les droits nécessaires +- AWS CLI configuré +- `eksctl` installé +- `kubectl` installé + +### 1. Construction de l’Image Docker + +```bash +# Construction de l’image Docker +docker build -t misterzapp/goofy-cdn:latest -f docker/cdn/Dockerfile . + +# Envoi de l’image sur Docker Hub +docker push misterzapp/goofy-cdn:latest +``` + +### 2. Déploiement sur EKS + +#### Création du Cluster + +```bash +eksctl create cluster \ + --name goofy-cdn-cluster \ + --region eu-west-3 \ + --nodegroup-name goofy-cdn-workers \ + --node-type t3.small \ + --nodes 2 \ + --nodes-min 1 \ + --nodes-max 3 +``` + +#### Déploiement de l’Application + +```bash +# Déploiement via Kubernetes +kubectl apply -f k8s/cdn-deployment.yaml +kubectl apply -f k8s/cdn-service.yaml + +# Vérification du déploiement +kubectl get pods +kubectl get services +``` + +### 3. Gestion des Ressources + +#### Vérification + +```bash +# Afficher les nœuds du cluster +kubectl get nodes + +# Lister tous les pods +kubectl get pods --all-namespaces + +# Afficher les logs des pods associés +kubectl logs -l app=goofy-cdn +``` + +#### Nettoyage + +```bash +# Supprimer le nodegroup +eksctl delete nodegroup --cluster goofy-cdn-cluster --name goofy-cdn-workers + +# Supprimer le cluster complet (pour éviter des coûts supplémentaires) +eksctl delete cluster --name goofy-cdn-cluster +``` + +### 4. Surveillance des Coûts AWS + +- **Cluster EKS** : environ 0,10 $ par heure +- **Nœuds EC2 (t3.small)** : environ 0,023 $ par heure par nœud +- **Load Balancer** : environ 0,025 $ par heure +- **Volumes EBS et ENI** : coûts variables selon l’utilisation + +⚠️ **Important** : Veillez à supprimer l’ensemble des ressources après usage pour éviter des frais inutiles. + +### 5. Dépannage Courant + +#### Problèmes de CNI + +```bash +# Réinstaller le CNI Amazon VPC +kubectl apply -f https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/v1.12.6/config/master/aws-k8s-cni.yaml + +# Redémarrer les pods du CNI +kubectl delete pods -n kube-system -l k8s-app=aws-node +``` + +#### Problèmes de Permissions + +Assurez-vous que le rôle IAM possède bien les politiques suivantes : + +- AmazonEKSClusterPolicy +- AmazonEKSServicePolicy +- AmazonEKSVPCResourceController +- AmazonEKS_CNI_Policy + +--- + +## 🖥 Déploiement Local avec Docker Desktop + +### Prérequis + +- Docker Desktop installé +- Kubernetes activé dans Docker Desktop (via kubeadm) +- `kubectl` installé (ex. : `brew install kubectl`) + +### 1. Configuration de Kubernetes dans Docker Desktop + +1. Ouvrez Docker Desktop +2. Rendez-vous dans **Settings > Kubernetes** +3. Cochez **Enable Kubernetes** +4. Sélectionnez **kubeadm** comme méthode de provisionnement +5. Cliquez sur **Apply & Restart** + +### 2. Construction de l’Image + +```bash +# Construire l’image localement +docker build -t goofy-cdn:local -f docker/cdn/Dockerfile . +``` + +### 3. Déploiement sur Kubernetes Local + +1. **Vérifier le Contexte de kubectl** : + + ```bash + # Afficher les contextes disponibles + kubectl config get-contexts + + # Utiliser le contexte Docker Desktop si nécessaire + kubectl config use-context docker-desktop + ``` + +2. **Déployer l’Application** : + + ```bash + # Appliquer les fichiers de configuration Kubernetes + kubectl apply -f k8s/cdn-deployment.yaml + kubectl apply -f k8s/cdn-service.yaml + + # Vérifier l’état des pods et services + kubectl get pods + kubectl get services + ``` + +### 4. Accès à l’Application + +L’application est accessible aux adresses suivantes : + +- **URL Principale** : [http://localhost:80](http://localhost:80) +- **Métriques** : [http://localhost:80/metrics](http://localhost:80/metrics) +- **Health Check** : [http://localhost:80/health](http://localhost:80/health) +- **Readiness** : [http://localhost:80/ready](http://localhost:80/ready) + +### 5. Commandes Utiles + +```bash +# Afficher les logs de l’application +kubectl logs -l app=goofy-cdn + +# Obtenir les détails d’un pod +kubectl describe pod -l app=goofy-cdn + +# Redémarrer les pods (après modification du code) +kubectl delete pod -l app=goofy-cdn + +# Supprimer le déploiement +kubectl delete -f k8s/cdn-deployment.yaml +kubectl delete -f k8s/cdn-service.yaml +``` + +### 6. Dépannage + +#### Pods en CrashLoopBackOff ou Erreur + +```bash +# Consulter les logs du pod +kubectl logs -l app=goofy-cdn + +# Afficher les détails et événements du pod +kubectl describe pod -l app=goofy-cdn +``` + +#### Service Inaccessible + +1. Vérifier que le service est bien créé : + ```bash + kubectl get services + ``` + +2. S’assurer que le pod est en état Ready : + ```bash + kubectl get pods -l app=goofy-cdn + ``` + +3. Visualiser les endpoints associés : + ```bash + kubectl get endpoints goofy-cdn-service + ``` + +#### Problèmes d’Image + +Si l’image n’est pas trouvée, vérifiez que : +1. L’image est bien construite localement : + ```bash + docker images | grep goofy-cdn + ``` +2. Le fichier de déploiement utilise le bon nom d’image : `image: goofy-cdn:local` diff --git a/app/CDN/internal/cache/cache.go b/app/CDN/internal/cache/cache.go deleted file mode 100644 index f2ba7ce..0000000 --- a/app/CDN/internal/cache/cache.go +++ /dev/null @@ -1,93 +0,0 @@ -// Package cache fournit des implémentations de cache pour le CDN -// Il propose deux types de cache : en mémoire (LRU) et Redis -package cache - -import ( - "context" - "github.com/hashicorp/golang-lru" - "github.com/redis/go-redis/v9" - "time" -) - -// Cache définit l'interface commune pour toutes les implémentations de cache -type Cache interface { - // Get récupère une valeur du cache à partir de sa clé - // Retourne la valeur et un booléen indiquant si la clé existe - Get(key string) (interface{}, bool) - - // Set stocke une valeur dans le cache avec la clé spécifiée - // Retourne une erreur si l'opération échoue - Set(key string, value interface{}) error - - // Delete supprime une valeur du cache à partir de sa clé - // Retourne une erreur si l'opération échoue - Delete(key string) error -} - -// MemoryCache implémente un cache en mémoire utilisant l'algorithme LRU -type MemoryCache struct { - lru *lru.Cache // Cache LRU sous-jacent -} - -// NewMemoryCache crée une nouvelle instance de MemoryCache avec une taille maximale spécifiée -// Retourne une erreur si la création du cache LRU échoue -func NewMemoryCache(size int) (*MemoryCache, error) { - l, err := lru.New(size) - if err != nil { - return nil, err - } - return &MemoryCache{lru: l}, nil -} - -// Get récupère une valeur du cache mémoire -func (m *MemoryCache) Get(key string) (interface{}, bool) { - return m.lru.Get(key) -} - -// Set ajoute ou met à jour une valeur dans le cache mémoire -func (m *MemoryCache) Set(key string, value interface{}) error { - m.lru.Add(key, value) - return nil -} - -// Delete supprime une valeur du cache mémoire -func (m *MemoryCache) Delete(key string) error { - m.lru.Remove(key) - return nil -} - -// RedisCache implémente un cache distribué utilisant Redis -type RedisCache struct { - client *redis.Client // Client Redis -} - -// NewRedisCache crée une nouvelle instance de RedisCache -// url: l'adresse du serveur Redis -// db: l'index de la base de données Redis à utiliser -func NewRedisCache(url string, db int) *RedisCache { - client := redis.NewClient(&redis.Options{ - Addr: url, - DB: db, - }) - return &RedisCache{client: client} -} - -// Get récupère une valeur du cache Redis -// Retourne nil, false si la clé n'existe pas ou en cas d'erreur -func (r *RedisCache) Get(key string) (interface{}, bool) { - val, err := r.client.Get(context.Background(), key).Result() - if err != nil { - return nil, false - } - return val, true -} - -// Set stocke une valeur dans Redis avec une expiration de 24 heures -func (r *RedisCache) Set(key string, value interface{}) error { - return r.client.Set(context.Background(), key, value, 24*time.Hour).Err() -} - -// Delete supprime une valeur du cache Redis -func (r *RedisCache) Delete(key string) error { - return r.client.Del(context.Background(), key).Err() -} diff --git a/app/CDN/internal/loadbalancer/loadbalancer.go b/app/CDN/internal/loadbalancer/loadbalancer.go deleted file mode 100644 index 3d55856..0000000 --- a/app/CDN/internal/loadbalancer/loadbalancer.go +++ /dev/null @@ -1,120 +0,0 @@ -// Package loadbalancer fournit différentes stratégies de répartition de charge -// pour distribuer le trafic entre plusieurs serveurs backend -package loadbalancer - -import ( - "sync" - "sync/atomic" -) - -// Backend représente un serveur backend avec ses propriétés -type Backend struct { - URL string // URL du serveur backend - Weight int // Poids pour l'algorithme weighted round-robin - CurrentWeight int // Poids actuel (utilisé dans l'algorithme weighted round-robin) - Connections int32 // Nombre de connexions actives (utilisé pour least connections) -} - -// LoadBalancer définit l'interface commune pour toutes les stratégies de load balancing -type LoadBalancer interface { - // NextBackend retourne le prochain backend à utiliser selon la stratégie choisie - NextBackend() *Backend -} - -// RoundRobin implémente la stratégie de répartition cyclique simple -type RoundRobin struct { - backends []*Backend // Liste des backends disponibles - current uint32 // Index du backend courant (accès atomique) -} - -// NewRoundRobin crée une nouvelle instance de RoundRobin -// urls: liste des URLs des serveurs backend -func NewRoundRobin(urls []string) *RoundRobin { - backends := make([]*Backend, len(urls)) - for i, url := range urls { - backends[i] = &Backend{URL: url} - } - return &RoundRobin{backends: backends} -} - -// NextBackend retourne le prochain backend dans l'ordre cyclique -// Utilise des opérations atomiques pour être thread-safe -func (r *RoundRobin) NextBackend() *Backend { - next := atomic.AddUint32(&r.current, 1) % uint32(len(r.backends)) - return r.backends[next] -} - -// WeightedRoundRobin implémente la stratégie de répartition pondérée -type WeightedRoundRobin struct { - backends []*Backend // Liste des backends avec leurs poids - mu sync.Mutex // Mutex pour la synchronisation -} - -// NewWeightedRoundRobin crée une nouvelle instance de WeightedRoundRobin -// urls: liste des URLs des serveurs backend -// weights: poids correspondants pour chaque serveur -func NewWeightedRoundRobin(urls []string, weights []int) *WeightedRoundRobin { - backends := make([]*Backend, len(urls)) - for i, url := range urls { - backends[i] = &Backend{ - URL: url, - Weight: weights[i], - CurrentWeight: weights[i], - } - } - return &WeightedRoundRobin{backends: backends} -} - -// NextBackend implémente l'algorithme de weighted round-robin -// Sélectionne le backend avec le plus grand poids actuel -func (w *WeightedRoundRobin) NextBackend() *Backend { - w.mu.Lock() - defer w.mu.Unlock() - - var best *Backend - var totalWeight int - - for _, b := range w.backends { - b.CurrentWeight += b.Weight - totalWeight += b.Weight - if best == nil || b.CurrentWeight > best.CurrentWeight { - best = b - } - } - - best.CurrentWeight -= totalWeight - return best -} - -// LeastConnections implémente la stratégie du nombre minimum de connexions -type LeastConnections struct { - backends []*Backend // Liste des backends -} - -// NewLeastConnections crée une nouvelle instance de LeastConnections -// urls: liste des URLs des serveurs backend -func NewLeastConnections(urls []string) *LeastConnections { - backends := make([]*Backend, len(urls)) - for i, url := range urls { - backends[i] = &Backend{URL: url} - } - return &LeastConnections{backends: backends} -} - -// NextBackend sélectionne le backend ayant le moins de connexions actives -// Utilise des opérations atomiques pour le comptage des connexions -func (l *LeastConnections) NextBackend() *Backend { - var best *Backend - var minConn int32 = -1 - - for _, b := range l.backends { - conn := atomic.LoadInt32(&b.Connections) - if minConn == -1 || conn < minConn { - minConn = conn - best = b - } - } - - atomic.AddInt32(&best.Connections, 1) - return best -} diff --git a/app/CDN/main.go b/app/CDN/main.go deleted file mode 100644 index e52a1d3..0000000 --- a/app/CDN/main.go +++ /dev/null @@ -1,120 +0,0 @@ -package main - -import ( - "app/internal/cache" - "app/internal/loadbalancer" - "app/internal/middleware" - "context" - "fmt" - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/sirupsen/logrus" - "net/http" - "os" - "os/signal" - "syscall" - "time" -) - -// main est la fonction principale qui initialise et démarre le serveur CDN -// Elle configure : -// - Le système de logging -// - Le cache en mémoire -// - Le load balancer -// - Les middlewares de sécurité et de monitoring -// - La gestion gracieuse de l'arrêt du serveur -func main() { - // Configuration du logger avec format JSON et niveau INFO - log := logrus.New() - log.SetFormatter(&logrus.JSONFormatter{}) - log.SetLevel(logrus.InfoLevel) - - // Initialisation du cache en mémoire avec une capacité de 1000 entrées - memCache, err := cache.NewMemoryCache(1000) - if err != nil { - log.Fatal(err) - } - - // Configuration du Load Balancer en mode Weighted Round Robin - // avec deux backends de même poids - backends := []string{"http://backend1:8080", "http://backend2:8080"} - weights := []int{1, 1} - lb := loadbalancer.NewWeightedRoundRobin(backends, weights) - - // Configuration du routeur HTTP - mux := http.NewServeMux() - - // Route principale qui gère le load balancing et le cache - // Pour chaque requête : - // 1. Vérifie si la réponse est en cache - // 2. Si non, proxie la requête vers un backend - // 3. Met en cache la réponse - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - backend := lb.NextBackend() - - // Tentative de récupération depuis le cache - if data, found := memCache.Get(r.URL.Path); found { - fmt.Fprint(w, data) - return - } - - // Proxy vers le backend sélectionné - resp, err := http.Get(backend.URL + r.URL.Path) - if err != nil { - http.Error(w, err.Error(), http.StatusBadGateway) - return - } - defer resp.Body.Close() - - // Mise en cache de la réponse pour les futures requêtes - memCache.Set(r.URL.Path, "cached response") - - fmt.Fprintf(w, "Proxied to %s", backend.URL) - }) - - // Exposition des métriques Prometheus pour le monitoring - mux.Handle("/metrics", promhttp.Handler()) - - // Application des middlewares dans l'ordre : - // 1. Sécurité (headers HTTPS, CORS, etc.) - // 2. Métriques (compteurs Prometheus) - // 3. Rate Limiting (100 req/s avec burst de 10) - handler := middleware.Security( - middleware.Metrics( - middleware.RateLimit(100, 10)(mux), - ), - ) - - // Configuration du serveur HTTP avec timeouts - srv := &http.Server{ - Addr: ":8080", - Handler: handler, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - MaxHeaderBytes: 1 << 20, // 1 MB - } - - // Démarrage du serveur dans une goroutine séparée - go func() { - log.Info("Starting server on :8080") - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatal(err) - } - }() - - // Configuration de la gestion gracieuse de l'arrêt - // Attend un signal SIGINT ou SIGTERM - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - - // Arrêt du serveur avec timeout de 30 secondes - log.Info("Shutting down server...") - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - if err := srv.Shutdown(ctx); err != nil { - log.Fatal("Server forced to shutdown:", err) - } - - log.Info("Server successfully shutdown") -} diff --git a/app/CDN/tmp/main b/app/CDN/tmp/main deleted file mode 100755 index 0ccf3ec..0000000 Binary files a/app/CDN/tmp/main and /dev/null differ diff --git a/app/back/.env b/app/back/.env index a4b3623..40363a6 100644 --- a/app/back/.env +++ b/app/back/.env @@ -1,3 +1,15 @@ -PORT=8080 -MONGO_URI=mongodb://mongodb:27017 +# MONGO_URI=mongodb://mongodb:27017 GIN_MODE=debug + +JWT_SECRET=votre_secret_jwt_tres_securise + +# Configuration des uploads +UPLOAD_DIR=./uploads +MAX_UPLOAD_SIZE=10485760 # 10MB en bytes + +# Configuration MongoDB +MONGO_DB_NAME=goofy_cdn +MONGO_USER=root +# MONGO_USER=admin +# MONGO_PASSWORD=password +MONGO_PASSWORD=root diff --git a/app/back/.env.example b/app/back/.env.example new file mode 100644 index 0000000..245ec87 --- /dev/null +++ b/app/back/.env.example @@ -0,0 +1,13 @@ +PORT=8082 +MONGO_URI=mongodb://mongodb:27017 +JWT_SECRET=votre_secret_jwt_tres_securise +GIN_MODE=debug + +# Configuration des uploads +UPLOAD_DIR=./uploads +MAX_UPLOAD_SIZE=10485760 # 10MB en bytes + +# Configuration MongoDB +MONGO_DB_NAME=goofy_cdn +MONGO_USER=admin +MONGO_PASSWORD=password diff --git a/app/back/go.mod b/app/back/go.mod index 0780dab..5b5562f 100644 --- a/app/back/go.mod +++ b/app/back/go.mod @@ -1,16 +1,20 @@ -module github.com/GoofyTeam/GoofyCDN/app/back +module app -go 1.21 +go 1.23 require ( github.com/gin-gonic/gin v1.9.1 + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/joho/godotenv v1.5.1 + github.com/stretchr/testify v1.8.3 go.mongodb.org/mongo-driver v1.13.1 + golang.org/x/crypto v0.9.0 ) require ( github.com/bytedance/sonic v1.9.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -27,6 +31,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect @@ -34,7 +39,6 @@ require ( github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.9.0 // indirect golang.org/x/net v0.10.0 // indirect golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect golang.org/x/sys v0.8.0 // indirect diff --git a/app/back/go.sum b/app/back/go.sum index 8454352..b8be3a6 100644 --- a/app/back/go.sum +++ b/app/back/go.sum @@ -23,6 +23,8 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= diff --git a/app/back/internal/api/test_routes.go b/app/back/internal/api/test_routes.go new file mode 100644 index 0000000..ded83d1 --- /dev/null +++ b/app/back/internal/api/test_routes.go @@ -0,0 +1,171 @@ +package api + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "fmt" + "math" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/gin-gonic/gin" +) + +// SetupTestRoutes configure les routes de test pour le backend +func SetupTestRoutes(r *gin.Engine) { + test := r.Group("/test") + { + // Test de cache avec contenu statique + test.GET("/cache/static/:id", func(c *gin.Context) { + id := c.Param("id") + content := fmt.Sprintf("Contenu statique pour l'ID %s - Timestamp: %d", id, time.Now().Unix()) + c.Header("Cache-Control", "public, max-age=60") + c.String(http.StatusOK, content) + }) + + // Test de latence avec différents patterns + test.GET("/latency/:pattern", func(c *gin.Context) { + pattern := c.Param("pattern") + var delay time.Duration + + switch pattern { + case "random": + delay = time.Duration(float64(time.Second) * (float64(time.Now().UnixNano()%1000) / 1000.0)) + case "spike": + if time.Now().UnixNano()%10 == 0 { // 10% de chance d'avoir un pic + delay = 2 * time.Second + } + case "wave": + t := float64(time.Now().Unix()) + // Génère une latence sinusoïdale entre 100ms et 1s + factor := (math.Sin(t/10) + 1) / 2 + delay = time.Duration(100+900*factor) * time.Millisecond + default: + delay = 100 * time.Millisecond + } + + time.Sleep(delay) + c.JSON(http.StatusOK, gin.H{ + "pattern": pattern, + "delay": delay.String(), + }) + }) + + // Test de téléchargement + test.GET("/download/:size", func(c *gin.Context) { + size := c.Param("size") + var fileSize int64 + + switch size { + case "small": + fileSize = 1 * 1024 * 1024 // 1MB + case "medium": + fileSize = 10 * 1024 * 1024 // 10MB + case "large": + fileSize = 100 * 1024 * 1024 // 100MB + default: + fileSize = 1 * 1024 * 1024 + } + + // Générer un nom de fichier aléatoire + randomBytes := make([]byte, 16) + rand.Read(randomBytes) + fileName := base64.URLEncoding.EncodeToString(randomBytes) + + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s.bin", fileName)) + c.Header("Content-Type", "application/octet-stream") + c.Header("Content-Length", fmt.Sprintf("%d", fileSize)) + c.Header("Cache-Control", "no-cache") + + // Envoyer les données par chunks + chunkSize := int64(64 * 1024) // 64KB chunks + remaining := fileSize + + for remaining > 0 { + if remaining < chunkSize { + chunkSize = remaining + } + chunk := make([]byte, chunkSize) + rand.Read(chunk) + c.Writer.Write(chunk) + c.Writer.Flush() + remaining -= chunkSize + time.Sleep(time.Millisecond * 10) // Simuler une latence réseau + } + }) + + // Test de compression + test.GET("/compression", func(c *gin.Context) { + // Générer un texte très compressible + var buffer bytes.Buffer + for i := 0; i < 1024*1024; i++ { // 1MB de données + buffer.WriteByte('a' + byte(i%26)) + } + c.String(http.StatusOK, buffer.String()) + }) + + // Test d'upload + test.POST("/upload", func(c *gin.Context) { + file, err := c.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Fichier manquant"}) + return + } + + // Créer le dossier uploads s'il n'existe pas + uploadDir := "uploads" + if err := os.MkdirAll(uploadDir, 0755); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la création du dossier"}) + return + } + + // Sauvegarder le fichier + dst := filepath.Join(uploadDir, file.Filename) + if err := c.SaveUploadedFile(file, dst); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la sauvegarde"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "status": "success", + "file": file.Filename, + "size": file.Size, + }) + }) + + // Test de streaming + test.GET("/stream/:seconds", func(c *gin.Context) { + c.Header("Content-Type", "text/event-stream") + c.Header("Cache-Control", "no-cache") + c.Header("Connection", "keep-alive") + c.Header("Transfer-Encoding", "chunked") + + duration := 10 // Durée par défaut en secondes + fmt.Sscanf(c.Param("seconds"), "%d", &duration) + + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + + for i := 0; i < duration; i++ { + select { + case <-ticker.C: + c.SSEvent("message", fmt.Sprintf("Événement %d/%d", i+1, duration)) + c.Writer.Flush() + case <-c.Request.Context().Done(): + return + } + } + }) + + // Endpoint de santé pour le CDN + test.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "status": "healthy", + "time": time.Now().Unix(), + }) + }) + } +} diff --git a/app/back/internal/handlers/auth.go b/app/back/internal/handlers/auth.go new file mode 100644 index 0000000..389e5e3 --- /dev/null +++ b/app/back/internal/handlers/auth.go @@ -0,0 +1,175 @@ +package handlers + +import ( + "app/internal/models" + "fmt" + "net/http" + "os" + "regexp" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "golang.org/x/crypto/bcrypt" +) + +type AuthHandler struct { + userCollection *mongo.Collection + folderCollection *mongo.Collection +} + +func NewAuthHandler(db *mongo.Database) *AuthHandler { + return &AuthHandler{ + userCollection: db.Collection("users"), + folderCollection: db.Collection("folders"), + } +} + +// isValidEmail vérifie si l'email est valide +func isValidEmail(email string) bool { + emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`) + return emailRegex.MatchString(email) +} + +// Register crée un nouveau compte utilisateur +func (h *AuthHandler) Register(c *gin.Context) { + var user models.User + if err := c.ShouldBindJSON(&user); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Validation de l'email + if !isValidEmail(user.Email) { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid email format"}) + return + } + + // Vérification si l'email existe déjà + var existingUser models.User + err := h.userCollection.FindOne(c, bson.M{"email": user.Email}).Decode(&existingUser) + if err == nil { + c.JSON(http.StatusConflict, gin.H{"error": "Email already exists"}) + return + } + + // Hashage du mot de passe + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"}) + return + } + + // Préparation de l'utilisateur pour l'insertion + now := time.Now() + user.Password = string(hashedPassword) + user.CreatedAt = now + user.UpdatedAt = now + + result, err := h.userCollection.InsertOne(c, user) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"}) + return + } + + // Récupération de l'ID généré + userID := result.InsertedID.(primitive.ObjectID) + user.ID = userID + + // Création du dossier racine pour l'utilisateur + rootFolder := models.Folder{ + Name: "root", + Path: "/", + UserID: userID, + Depth: 0, + CreatedAt: now, + UpdatedAt: now, + } + + // Insertion du dossier racine + _, err = h.folderCollection.InsertOne(c, rootFolder) + if err != nil { + _, deleteErr := h.userCollection.DeleteOne(c, bson.M{"_id": userID}) + if deleteErr != nil { + fmt.Printf("Erreur lors de la suppression de l'utilisateur: %v\n", deleteErr) + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create root folder"}) + return + } + + // token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + // "user_id": user.ID.Hex(), + // "email": user.Email, + // "exp": time.Now().Add(time.Hour * 24).Unix(), // Token valide 24h + // }) + + // tokenString, err := token.SignedString([]byte(os.Getenv("JWT_SECRET"))) + // if err != nil { + // c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la génération du token"}) + // return + // } + + user.Password = "" // On ne renvoie pas le mot de passe + c.JSON(http.StatusCreated, user) + // c.JSON(http.StatusCreated, gin.H{ + // "token": tokenString, + // "user": user, + // }) +} + +// Login authentifie un utilisateur et renvoie un token JWT +func (h *AuthHandler) Login(c *gin.Context) { + var loginReq models.LoginRequest + if err := c.ShouldBindJSON(&loginReq); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Validation de l'email + if !isValidEmail(loginReq.Email) { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid email format"}) + return + } + + var user models.User + err := h.userCollection.FindOne(c, bson.M{"email": loginReq.Email}).Decode(&user) + if err != nil { + if err == mongo.ErrNoDocuments { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Utilisateur non trouvé"}) + } else { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Erreur lors de la recherche de l'utilisateur: " + err.Error()}) + } + return + } + + // Log pour le débogage + fmt.Printf("Login - Stored hash length: %d, Input password length: %d\n", len(user.Password), len(loginReq.Password)) + fmt.Printf("Login - Input password: %s\n", loginReq.Password) + + // Vérification du mot de passe + err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(loginReq.Password)) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Mot de passe incorrect"}) + return + } + + // Création du token JWT + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "user_id": user.ID.Hex(), + "email": user.Email, + "exp": time.Now().Add(time.Hour * 24).Unix(), + }) + + tokenString, err := token.SignedString([]byte(os.Getenv("JWT_SECRET"))) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "token": tokenString, + }) +} diff --git a/app/back/internal/handlers/auth_test.go b/app/back/internal/handlers/auth_test.go new file mode 100644 index 0000000..f3def2a --- /dev/null +++ b/app/back/internal/handlers/auth_test.go @@ -0,0 +1,213 @@ +package handlers + +import ( + "app/internal/models" + "bytes" + "context" + "encoding/json" + "log" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "golang.org/x/crypto/bcrypt" +) + +var testDB *mongo.Database + +func setupTestDB() *mongo.Database { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017")) + if err != nil { + log.Fatal("Erreur de connexion à MongoDB:", err) + } + + db := client.Database("goofycdn_test") + return db +} + +func cleanupTestDB(db *mongo.Database) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Suppression de toutes les collections de test + if err := db.Collection("users").Drop(ctx); err != nil { + log.Printf("Erreur lors du nettoyage de la collection users: %v", err) + } +} + +func TestMain(m *testing.M) { + // Initialisation de la base de données de test + testDB = setupTestDB() + + // Exécution des tests + code := m.Run() + + // Nettoyage après les tests + cleanupTestDB(testDB) + + os.Exit(code) +} + +func clearCollection(t *testing.T, collection *mongo.Collection) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err := collection.DeleteMany(ctx, bson.M{}) + if err != nil { + t.Fatalf("Erreur lors du nettoyage de la collection: %v", err) + } +} + +// createTestUser crée un utilisateur de test dans la base de données +func createTestUser(t *testing.T, h *AuthHandler, email, password string) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + t.Fatalf("Erreur lors du hashage du mot de passe: %v", err) + } + + user := models.User{ + Email: email, + Password: string(hashedPassword), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + _, err = h.userCollection.InsertOne(context.Background(), user) + if err != nil { + t.Fatalf("Erreur lors de la création de l'utilisateur de test: %v", err) + } +} + +func TestAuthHandler_Register(t *testing.T) { + // Nettoyage de la collection avant le test + clearCollection(t, testDB.Collection("users")) + + // Configuration du mode test pour Gin + gin.SetMode(gin.TestMode) + + // Création d'un router de test + r := gin.Default() + h := NewAuthHandler(testDB) + r.POST("/register", h.Register) + + tests := []struct { + name string + input models.User + wantStatus int + }{ + { + name: "Valid registration", + input: models.User{ + // Username: "testuser", + Email: "test@example.com", + Password: "password123", + }, + wantStatus: http.StatusCreated, + }, + { + name: "Invalid email", + input: models.User{ + // Username: "testuser2", + Email: "invalid-email", + Password: "password123", + }, + wantStatus: http.StatusBadRequest, + }, + // Ajoutez d'autres cas de test selon vos besoins + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Conversion de l'input en JSON + jsonInput, err := json.Marshal(tt.input) + assert.NoError(t, err) + + // Création de la requête + req := httptest.NewRequest("POST", "/register", bytes.NewBuffer(jsonInput)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + // Exécution de la requête + r.ServeHTTP(w, req) + + // Vérification du statut + assert.Equal(t, tt.wantStatus, w.Code) + }) + } +} + +func TestAuthHandler_Login(t *testing.T) { + // Configuration du mode test pour Gin + gin.SetMode(gin.TestMode) + + // Création d'un router de test + r := gin.Default() + h := NewAuthHandler(testDB) + r.POST("/login", h.Login) + + tests := []struct { + name string + input models.LoginRequest + setupUser bool + wantStatus int + }{ + { + name: "Valid login", + input: models.LoginRequest{ + Email: "test@example.com", + Password: "password123", + }, + setupUser: true, + wantStatus: http.StatusOK, + }, + { + name: "Invalid credentials", + input: models.LoginRequest{ + Email: "test@example.com", + Password: "wrongpassword", + }, + setupUser: true, + wantStatus: http.StatusUnauthorized, + }, + { + name: "Invalid email format", + input: models.LoginRequest{ + Email: "invalid-email", + Password: "password123", + }, + setupUser: false, + wantStatus: http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Nettoyage de la collection avant chaque test + clearCollection(t, testDB.Collection("users")) + + if tt.setupUser { + createTestUser(t, h, tt.input.Email, "password123") + } + + body, _ := json.Marshal(tt.input) + req := httptest.NewRequest("POST", "/login", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + + // Enregistrement de la réponse + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + assert.Equal(t, tt.wantStatus, w.Code) + }) + } +} diff --git a/app/back/internal/handlers/file.go b/app/back/internal/handlers/file.go new file mode 100644 index 0000000..fe7bb2e --- /dev/null +++ b/app/back/internal/handlers/file.go @@ -0,0 +1,185 @@ +package handlers + +import ( + "app/internal/models" + "io" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +type FileHandler struct { + fileCollection *mongo.Collection + folderCollection *mongo.Collection + uploadDir string +} + +func NewFileHandler(db *mongo.Database, uploadDir string) *FileHandler { + return &FileHandler{ + fileCollection: db.Collection("files"), + folderCollection: db.Collection("folders"), + uploadDir: uploadDir, + } +} + +// UploadFile gère l'upload d'un fichier +func (h *FileHandler) UploadFile(c *gin.Context) { + // Récupération du fichier depuis la requête multipart + file, header, err := c.Request.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"}) + return + } + defer file.Close() + + userID, _ := c.Get("user_id") + userIDObj, _ := userID.(primitive.ObjectID) + + // Récupération de l'ID du dossier parent + var folder models.Folder + folderIDStr := c.PostForm("folder_id") + if folderIDStr == "" { + // Si aucun dossier n'est spécifié, on utilise le dossier racine + err = h.folderCollection.FindOne(c, bson.M{ + "user_id": userIDObj, + "name": "root", + "depth": 0, + }).Decode(&folder) + } else { + folderID, err := primitive.ObjectIDFromHex(folderIDStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid folder ID format"}) + return + } + // Vérification que le dossier existe et appartient à l'utilisateur + err = h.folderCollection.FindOne(c, bson.M{ + "_id": folderID, + "user_id": userIDObj, + }).Decode(&folder) + } + + if err != nil { + if err == mongo.ErrNoDocuments { + c.JSON(http.StatusNotFound, gin.H{"error": "Folder not found"}) + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error finding folder"}) + } + return + } + + // Création du chemin de stockage + userDir := filepath.Join(h.uploadDir, userIDObj.Hex()) + if err := os.MkdirAll(userDir, 0755); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create upload directory"}) + return + } + + // Génération d'un nom de fichier unique + filename := primitive.NewObjectID().Hex() + filepath.Ext(header.Filename) + filePath := filepath.Join(userDir, filename) + + // Sauvegarde du fichier sur le disque + out, err := os.Create(filePath) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create file"}) + return + } + defer out.Close() + + _, err = io.Copy(out, file) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) + return + } + + // Création de l'entrée dans la base de données + fileDoc := models.File{ + Name: header.Filename, + Path: filePath, + Size: header.Size, + MimeType: header.Header.Get("Content-Type"), + FolderID: folder.ID, + UserID: userIDObj, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + result, err := h.fileCollection.InsertOne(c, fileDoc) + if err != nil { + // Suppression du fichier en cas d'erreur + os.Remove(filePath) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file metadata"}) + return + } + + fileDoc.ID = result.InsertedID.(primitive.ObjectID) + c.JSON(http.StatusCreated, fileDoc) +} + +// GetFile récupère un fichier +func (h *FileHandler) GetFile(c *gin.Context) { + fileID, err := primitive.ObjectIDFromHex(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid file ID"}) + return + } + + userID, _ := c.Get("user_id") + + var file models.File + err = h.fileCollection.FindOne(c, bson.M{ + "_id": fileID, + "user_id": userID, + }).Decode(&file) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "File not found"}) + return + } + + c.File(file.Path) +} + +// DeleteFile supprime un fichier +func (h *FileHandler) DeleteFile(c *gin.Context) { + fileID, err := primitive.ObjectIDFromHex(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid file ID"}) + return + } + + userID, _ := c.Get("user_id") + + var file models.File + err = h.fileCollection.FindOne(c, bson.M{ + "_id": fileID, + "user_id": userID, + }).Decode(&file) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "File not found"}) + return + } + + // Suppression du fichier physique + if err := os.Remove(file.Path); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file"}) + return + } + + // Suppression de l'entrée dans la base de données + _, err = h.fileCollection.DeleteOne(c, bson.M{ + "_id": fileID, + "user_id": userID, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file metadata"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "File deleted successfully"}) +} diff --git a/app/back/internal/handlers/file_test.go b/app/back/internal/handlers/file_test.go new file mode 100644 index 0000000..e6fdd28 --- /dev/null +++ b/app/back/internal/handlers/file_test.go @@ -0,0 +1,258 @@ +package handlers + +import ( + "app/internal/models" + "bytes" + "context" + "mime/multipart" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func setupFileTest(t *testing.T) (*FileHandler, *gin.Engine, string) { + // Création d'un dossier temporaire pour les uploads + tempDir, err := os.MkdirTemp("", "file_test") + if err != nil { + t.Fatalf("Erreur lors de la création du dossier temporaire: %v", err) + } + + // Configuration du handler + h := NewFileHandler(testDB, tempDir) + gin.SetMode(gin.TestMode) + r := gin.Default() + + return h, r, tempDir +} + +func createTestFolder(t *testing.T, h *FileHandler, userID primitive.ObjectID) primitive.ObjectID { + folder := models.Folder{ + Name: "Test Folder", + UserID: userID, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + result, err := h.folderCollection.InsertOne(context.Background(), folder) + if err != nil { + t.Fatalf("Erreur lors de la création du dossier de test: %v", err) + } + + return result.InsertedID.(primitive.ObjectID) +} + +func TestFileHandler_UploadFile(t *testing.T) { + h, r, tempDir := setupFileTest(t) + defer os.RemoveAll(tempDir) + + userID := primitive.NewObjectID() + folderID := createTestFolder(t, h, userID) + + r.POST("/upload", func(c *gin.Context) { + c.Set("user_id", userID) + h.UploadFile(c) + }) + + tests := []struct { + name string + setup func() (*bytes.Buffer, *multipart.Writer) + wantStatus int + }{ + { + name: "Valid upload", + setup: func() (*bytes.Buffer, *multipart.Writer) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, _ := writer.CreateFormFile("file", "test.txt") + part.Write([]byte("test content")) + writer.WriteField("folder_id", folderID.Hex()) + writer.Close() + return body, writer + }, + wantStatus: http.StatusCreated, + }, + { + name: "Missing file", + setup: func() (*bytes.Buffer, *multipart.Writer) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + writer.WriteField("folder_id", folderID.Hex()) + writer.Close() + return body, writer + }, + wantStatus: http.StatusBadRequest, + }, + { + name: "Invalid folder ID", + setup: func() (*bytes.Buffer, *multipart.Writer) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, _ := writer.CreateFormFile("file", "test.txt") + part.Write([]byte("test content")) + writer.WriteField("folder_id", "invalid-id") + writer.Close() + return body, writer + }, + wantStatus: http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + body, writer := tt.setup() + req := httptest.NewRequest("POST", "/upload", body) + req.Header.Set("Content-Type", writer.FormDataContentType()) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + assert.Equal(t, tt.wantStatus, w.Code) + }) + } +} + +func TestFileHandler_GetFile(t *testing.T) { + h, r, tempDir := setupFileTest(t) + defer os.RemoveAll(tempDir) + + userID := primitive.NewObjectID() + folderID := createTestFolder(t, h, userID) + + // Création d'un fichier de test + testFilePath := filepath.Join(tempDir, "test.txt") + err := os.WriteFile(testFilePath, []byte("test content"), 0644) + assert.NoError(t, err) + + file := models.File{ + Name: "test.txt", + Path: testFilePath, + Size: 12, + MimeType: "text/plain", + FolderID: folderID, + UserID: userID, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + result, err := h.fileCollection.InsertOne(context.Background(), file) + assert.NoError(t, err) + fileID := result.InsertedID.(primitive.ObjectID) + + r.GET("/files/:id", func(c *gin.Context) { + c.Set("user_id", userID) + h.GetFile(c) + }) + + tests := []struct { + name string + fileID string + wantStatus int + }{ + { + name: "Valid file", + fileID: fileID.Hex(), + wantStatus: http.StatusOK, + }, + { + name: "Invalid file ID", + fileID: "invalid-id", + wantStatus: http.StatusBadRequest, + }, + { + name: "Non-existent file", + fileID: primitive.NewObjectID().Hex(), + wantStatus: http.StatusNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest("GET", "/files/"+tt.fileID, nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + assert.Equal(t, tt.wantStatus, w.Code) + }) + } +} + +func TestFileHandler_DeleteFile(t *testing.T) { + h, r, tempDir := setupFileTest(t) + defer os.RemoveAll(tempDir) + + userID := primitive.NewObjectID() + folderID := createTestFolder(t, h, userID) + + // Création d'un fichier de test + testFilePath := filepath.Join(tempDir, "test.txt") + err := os.WriteFile(testFilePath, []byte("test content"), 0644) + assert.NoError(t, err) + + file := models.File{ + Name: "test.txt", + Path: testFilePath, + Size: 12, + MimeType: "text/plain", + FolderID: folderID, + UserID: userID, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + result, err := h.fileCollection.InsertOne(context.Background(), file) + assert.NoError(t, err) + fileID := result.InsertedID.(primitive.ObjectID) + + r.DELETE("/files/:id", func(c *gin.Context) { + c.Set("user_id", userID) + h.DeleteFile(c) + }) + + tests := []struct { + name string + fileID string + wantStatus int + }{ + { + name: "Valid deletion", + fileID: fileID.Hex(), + wantStatus: http.StatusOK, + }, + { + name: "Invalid file ID", + fileID: "invalid-id", + wantStatus: http.StatusBadRequest, + }, + { + name: "Non-existent file", + fileID: primitive.NewObjectID().Hex(), + wantStatus: http.StatusNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest("DELETE", "/files/"+tt.fileID, nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + assert.Equal(t, tt.wantStatus, w.Code) + + if tt.wantStatus == http.StatusOK { + // Vérifier que le fichier a bien été supprimé de la base de données + var count int64 + count, err = h.fileCollection.CountDocuments(context.Background(), bson.M{"_id": fileID}) + assert.NoError(t, err) + assert.Equal(t, int64(0), count) + + // Vérifier que le fichier a bien été supprimé du système de fichiers + _, err = os.Stat(testFilePath) + assert.True(t, os.IsNotExist(err)) + } + }) + } +} diff --git a/app/back/internal/handlers/folder.go b/app/back/internal/handlers/folder.go new file mode 100644 index 0000000..f940376 --- /dev/null +++ b/app/back/internal/handlers/folder.go @@ -0,0 +1,252 @@ +package handlers + +import ( + "app/internal/models" + "net/http" + "path" + "strings" + "time" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +type FolderHandler struct { + folderCollection *mongo.Collection + fileCollection *mongo.Collection +} + +func NewFolderHandler(db *mongo.Database) *FolderHandler { + return &FolderHandler{ + folderCollection: db.Collection("folders"), + fileCollection: db.Collection("files"), + } +} + +// CreateFolder crée un nouveau dossier +func (h *FolderHandler) CreateFolder(c *gin.Context) { + var folder models.Folder + if err := c.ShouldBindJSON(&folder); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Validation du nom du dossier + if folder.Name == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Folder name cannot be empty"}) + return + } + + // Récupération de l'ID utilisateur depuis le token JWT + userID, _ := c.Get("user_id") + folder.UserID = userID.(primitive.ObjectID) + + // Vérification que le nom du dossier n'existe pas déjà pour cet utilisateur + var existingFolder models.Folder + err := h.folderCollection.FindOne(c, bson.M{ + "name": folder.Name, + "user_id": folder.UserID, + }).Decode(&existingFolder) + if err == nil { + c.JSON(http.StatusConflict, gin.H{"error": "Un dossier avec ce nom existe déjà"}) + return + } else if err != mongo.ErrNoDocuments { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la vérification du nom du dossier"}) + return + } + + // Vérification de la profondeur maximale + if folder.ParentID != nil { + var parentFolder models.Folder + err := h.folderCollection.FindOne(c, bson.M{"_id": folder.ParentID}).Decode(&parentFolder) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Parent folder not found"}) + return + } + + // Construction du chemin complet + folder.Path = path.Join(parentFolder.Path, folder.Name) + + // Vérification de la profondeur maximale 10 + if len(strings.Split(folder.Path, "/")) > 10 { + c.JSON(http.StatusBadRequest, gin.H{"error": "Profondeur maximale de dossiers atteinte"}) + return + } + folder.Depth = parentFolder.Depth + 1 + } else { + folder.Depth = 0 + folder.Path = "/" + folder.Name + } + + folder.CreatedAt = time.Now() + folder.UpdatedAt = time.Now() + + // Insertion du dossier dans la base de données + result, err := h.folderCollection.InsertOne(c, folder) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create folder"}) + return + } + + folder.ID = result.InsertedID.(primitive.ObjectID) + c.JSON(http.StatusCreated, folder) +} + +// ListFolderContents liste le contenu d'un dossier +func (h *FolderHandler) ListFolderContents(c *gin.Context) { + folderName := c.Param("name") + userID, _ := c.Get("user_id") + userIDObj := userID.(primitive.ObjectID) + + // Récupération du dossier par son nom + var folder models.Folder + err := h.folderCollection.FindOne(c, bson.M{ + "name": folderName, + "user_id": userIDObj, + }).Decode(&folder) + if err != nil { + if err == mongo.ErrNoDocuments { + c.JSON(http.StatusNotFound, gin.H{"error": "Folder not found"}) + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to find folder"}) + } + return + } + + // Récupération des sous-dossiers + var folders []models.Folder + folderCursor, err := h.folderCollection.Find(c, bson.M{ + "parent_id": folder.ID, + "user_id": userIDObj, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list folders"}) + return + } + if err = folderCursor.All(c, &folders); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode folders"}) + return + } + + // Récupération des fichiers + var files []models.File + fileCursor, err := h.fileCollection.Find(c, bson.M{ + "folder_id": folder.ID, + "user_id": userIDObj, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list files"}) + return + } + if err = fileCursor.All(c, &files); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode files"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "folders": folders, + "files": files, + }) +} + +// ListAllFolders liste tous les dossiers de l'utilisateur +func (h *FolderHandler) ListAllFolders(c *gin.Context) { + // Récupérer l'ID de l'utilisateur depuis le contexte + userID, exists := c.Get("user_id") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User ID not found in context"}) + return + } + + // L'ID est déjà un ObjectID depuis le middleware + objectID, ok := userID.(primitive.ObjectID) + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID type"}) + return + } + + // Trouver tous les dossiers de l'utilisateur + cursor, err := h.folderCollection.Find(c, bson.M{"user_id": objectID}) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch folders"}) + return + } + defer cursor.Close(c) + + var folders []models.Folder + if err := cursor.All(c, &folders); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode folders"}) + return + } + + c.JSON(http.StatusOK, folders) +} + +// DeleteFolder supprime un dossier et son contenu +func (h *FolderHandler) DeleteFolder(c *gin.Context) { + folderName := c.Param("name") + userID, _ := c.Get("user_id") + userIDObj := userID.(primitive.ObjectID) + + // Récupération du dossier par son nom + var folder models.Folder + err := h.folderCollection.FindOne(c, bson.M{ + "name": folderName, + "user_id": userIDObj, + }).Decode(&folder) + if err != nil { + if err == mongo.ErrNoDocuments { + c.JSON(http.StatusNotFound, gin.H{"error": "Folder not found"}) + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to find folder"}) + } + return + } + + // Vérification que ce n'est pas le dossier racine + if folder.Name == "root" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete root folder"}) + return + } + + // Suppression récursive des sous-dossiers et fichiers + if err := h.deleteSubFoldersAndFiles(c, folder.ID, userIDObj); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete folder contents"}) + return + } + + // Suppression du dossier lui-même + _, err = h.folderCollection.DeleteOne(c, bson.M{ + "_id": folder.ID, + "user_id": userIDObj, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete folder"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Folder deleted successfully"}) +} + +func (h *FolderHandler) deleteSubFoldersAndFiles(c *gin.Context, folderID primitive.ObjectID, userID primitive.ObjectID) error { + // Suppression récursive des sous-dossiers et fichiers + _, err := h.folderCollection.DeleteMany(c, bson.M{ + "path": bson.M{"$regex": "^" + folderID.Hex() + "/"}, + "user_id": userID, + }) + if err != nil { + return err + } + + _, err = h.fileCollection.DeleteMany(c, bson.M{ + "folder_id": folderID, + "user_id": userID, + }) + if err != nil { + return err + } + + return nil +} diff --git a/app/back/internal/handlers/folder_test.go b/app/back/internal/handlers/folder_test.go new file mode 100644 index 0000000..7ab7ff3 --- /dev/null +++ b/app/back/internal/handlers/folder_test.go @@ -0,0 +1,311 @@ +package handlers + +import ( + "app/internal/models" + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func setupFolderTest(t *testing.T) (*FolderHandler, *gin.Engine) { + h := NewFolderHandler(testDB) + gin.SetMode(gin.TestMode) + r := gin.Default() + return h, r +} + +func TestFolderHandler_CreateFolder(t *testing.T) { + h, r := setupFolderTest(t) + clearCollection(t, h.folderCollection) + + userID := primitive.NewObjectID() + r.POST("/folders", func(c *gin.Context) { + c.Set("user_id", userID) + h.CreateFolder(c) + }) + + tests := []struct { + name string + input models.Folder + wantStatus int + }{ + { + name: "Valid root folder", + input: models.Folder{ + Name: "Root Folder", + }, + wantStatus: http.StatusCreated, + }, + { + name: "Empty name", + input: models.Folder{ + Name: "", + }, + wantStatus: http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + body, err := json.Marshal(tt.input) + assert.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/folders", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + r.ServeHTTP(w, req) + assert.Equal(t, tt.wantStatus, w.Code) + + if w.Code == http.StatusCreated { + var response models.Folder + err = json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, tt.input.Name, response.Name) + assert.Equal(t, userID, response.UserID) + assert.Equal(t, 0, response.Depth) + assert.Equal(t, "/"+tt.input.Name, response.Path) + } + }) + } +} + +func TestFolderHandler_CreateSubFolder(t *testing.T) { + h, r := setupFolderTest(t) + clearCollection(t, h.folderCollection) + + userID := primitive.NewObjectID() + + // Création d'un dossier parent + parentFolder := models.Folder{ + Name: "Parent", + UserID: userID, + Depth: 0, + Path: "/Parent", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + result, err := h.folderCollection.InsertOne(context.Background(), parentFolder) + assert.NoError(t, err) + parentID := result.InsertedID.(primitive.ObjectID) + + r.POST("/folders", func(c *gin.Context) { + c.Set("user_id", userID) + h.CreateFolder(c) + }) + + tests := []struct { + name string + input models.Folder + wantStatus int + }{ + { + name: "Valid subfolder", + input: models.Folder{ + Name: "Subfolder", + ParentID: &parentID, + }, + wantStatus: http.StatusCreated, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + body, err := json.Marshal(tt.input) + assert.NoError(t, err) + + req := httptest.NewRequest("POST", "/folders", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + r.ServeHTTP(w, req) + assert.Equal(t, tt.wantStatus, w.Code) + + if tt.wantStatus == http.StatusCreated { + var response models.Folder + err = json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, tt.input.Name, response.Name) + assert.Equal(t, userID, response.UserID) + assert.Equal(t, 1, response.Depth) + assert.Equal(t, "/Parent/Subfolder", response.Path) + } + }) + } +} + +func TestFolderHandler_ListFolderContents(t *testing.T) { + h, r := setupFolderTest(t) + clearCollection(t, h.folderCollection) + clearCollection(t, h.fileCollection) + + userID := primitive.NewObjectID() + + // Création d'un dossier parent + parentFolder := models.Folder{ + Name: "Parent", + UserID: userID, + Depth: 0, + Path: "/Parent", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + result, err := h.folderCollection.InsertOne(context.Background(), parentFolder) + assert.NoError(t, err) + parentID := result.InsertedID.(primitive.ObjectID) + + // Création d'un sous-dossier + subFolder := models.Folder{ + Name: "Subfolder", + UserID: userID, + ParentID: &parentID, + Depth: 1, + Path: "/Parent/Subfolder", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + _, err = h.folderCollection.InsertOne(context.Background(), subFolder) + assert.NoError(t, err) + + r.GET("/folders/:id", func(c *gin.Context) { + c.Set("user_id", userID) + h.ListFolderContents(c) + }) + + tests := []struct { + name string + folderID string + wantStatus int + wantCount int + }{ + { + name: "Valid folder", + folderID: parentID.Hex(), + wantStatus: http.StatusOK, + wantCount: 1, + }, + { + name: "Invalid folder ID", + folderID: "invalid-id", + wantStatus: http.StatusBadRequest, + }, + { + name: "Non-existent folder", + folderID: primitive.NewObjectID().Hex(), + wantStatus: http.StatusOK, + wantCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest("GET", "/folders/"+tt.folderID, nil) + w := httptest.NewRecorder() + + r.ServeHTTP(w, req) + assert.Equal(t, tt.wantStatus, w.Code) + + if tt.wantStatus == http.StatusOK { + var response struct { + Folders []models.Folder `json:"folders"` + Files []models.File `json:"files"` + } + err = json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, tt.wantCount, len(response.Folders)) + } + }) + } +} + +func TestFolderHandler_DeleteFolder(t *testing.T) { + h, r := setupFolderTest(t) + clearCollection(t, h.folderCollection) + clearCollection(t, h.fileCollection) + + userID := primitive.NewObjectID() + + // Création d'un dossier parent + parentFolder := models.Folder{ + Name: "Parent", + UserID: userID, + Depth: 0, + Path: "/Parent", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + result, err := h.folderCollection.InsertOne(context.Background(), parentFolder) + assert.NoError(t, err) + parentID := result.InsertedID.(primitive.ObjectID) + + // Création d'un sous-dossier + subFolder := models.Folder{ + Name: "Subfolder", + UserID: userID, + ParentID: &parentID, + Depth: 1, + Path: "/Parent/Subfolder", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + _, err = h.folderCollection.InsertOne(context.Background(), subFolder) + assert.NoError(t, err) + + r.DELETE("/folders/:id", func(c *gin.Context) { + c.Set("user_id", userID) + h.DeleteFolder(c) + }) + + tests := []struct { + name string + folderID string + wantStatus int + }{ + { + name: "Valid deletion", + folderID: parentID.Hex(), + wantStatus: http.StatusOK, + }, + { + name: "Invalid folder ID", + folderID: "invalid-id", + wantStatus: http.StatusBadRequest, + }, + { + name: "Non-existent folder", + folderID: primitive.NewObjectID().Hex(), + wantStatus: http.StatusNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest("DELETE", "/folders/"+tt.folderID, nil) + w := httptest.NewRecorder() + + r.ServeHTTP(w, req) + assert.Equal(t, tt.wantStatus, w.Code) + + if tt.wantStatus == http.StatusOK { + // Vérifier que le dossier et ses sous-dossiers ont été supprimés + count, err := h.folderCollection.CountDocuments(context.Background(), bson.M{ + "$or": []bson.M{ + {"_id": parentID}, + {"path": bson.M{"$regex": "^/Parent/"}}, + }, + }) + assert.NoError(t, err) + assert.Equal(t, int64(0), count) + } + }) + } +} diff --git a/app/back/internal/handlers/health.go b/app/back/internal/handlers/health.go new file mode 100644 index 0000000..6275db8 --- /dev/null +++ b/app/back/internal/handlers/health.go @@ -0,0 +1,27 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +type HealthHandler struct{} + +func NewHealthHandler() *HealthHandler { + return &HealthHandler{} +} + +// Health godoc +// @Summary Vérifie l'état de santé du serveur +// @Description Retourne un statut 200 si le serveur est en bon état +// @Tags health +// @Produce json +// @Success 200 {object} map[string]string +// @Router /health [get] +func (h *HealthHandler) Health(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "status": "UP", + "service": "backend", + }) +} diff --git a/app/back/internal/middleware/auth.go b/app/back/internal/middleware/auth.go new file mode 100644 index 0000000..3f6b091 --- /dev/null +++ b/app/back/internal/middleware/auth.go @@ -0,0 +1,51 @@ +package middleware + +import ( + "net/http" + "os" + "strings" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// AuthMiddleware vérifie le token JWT et ajoute l'ID utilisateur au contexte +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) + c.Abort() + return + } + + tokenString := strings.Replace(authHeader, "Bearer ", "", 1) + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return []byte(os.Getenv("JWT_SECRET")), nil + }) + + if err != nil || !token.Valid { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) + c.Abort() + return + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"}) + c.Abort() + return + } + + userID, err := primitive.ObjectIDFromHex(claims["user_id"].(string)) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user ID in token"}) + c.Abort() + return + } + + c.Set("user_id", userID) + c.Next() + } +} diff --git a/app/back/internal/middleware/cors.go b/app/back/internal/middleware/cors.go new file mode 100644 index 0000000..e907de1 --- /dev/null +++ b/app/back/internal/middleware/cors.go @@ -0,0 +1,85 @@ +package middleware + +import ( + "os" + "strings" + + "github.com/gin-gonic/gin" +) + +func CORSMiddleware() gin.HandlerFunc { + allowedOrigins := getAllowedOrigins() + + return func(c *gin.Context) { + origin := c.Request.Header.Get("Origin") + + // Si l'origine est autorisée ou en mode développement + if isAllowedOrigin(origin, allowedOrigins) { + c.Writer.Header().Set("Access-Control-Allow-Origin", origin) + } else { + c.Writer.Header().Set("Access-Control-Allow-Origin", allowedOrigins[0]) // Par défaut + } + + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE") + c.Writer.Header().Set("Access-Control-Max-Age", "86400") // 24 heures + + if c.Request.Method == "OPTIONS" { + c.Writer.Header().Set("Access-Control-Max-Age", "86400") + c.AbortWithStatus(204) + return + } + + c.Next() + } +} + +func getAllowedOrigins() []string { + // En développement, autoriser localhost + if gin.Mode() == gin.DebugMode { + return []string{ + "http://localhost:5173", // Vite dev server + "http://localhost:5174", // Vite dev server + "http://localhost:5175", // Vite dev server + "http://localhost:3000", // Autre port courant + "http://localhost:3001", // Autre port courant + "http://localhost:8080", // Backend + "http://localhost", // Backend + "http://127.0.0.1:5173", + "http://127.0.0.1:3000", + "http://127.0.0.1:8080", + "http://127.0.0.1", + } + } + + // En production, utiliser les origines définies dans les variables d'environnement + allowedOrigins := os.Getenv("ALLOWED_ORIGINS") + if allowedOrigins == "" { + return []string{"*"} // Fallback, à modifier en production + } + + return strings.Split(allowedOrigins, ",") +} + +func isAllowedOrigin(origin string, allowedOrigins []string) bool { + if origin == "" { + return false + } + + // Si "*" est dans la liste, tout est autorisé + for _, allowed := range allowedOrigins { + if allowed == "*" { + return true + } + } + + // Sinon, vérifier si l'origine est dans la liste + for _, allowed := range allowedOrigins { + if allowed == origin { + return true + } + } + + return false +} diff --git a/app/back/internal/models/file.go b/app/back/internal/models/file.go new file mode 100644 index 0000000..1612f71 --- /dev/null +++ b/app/back/internal/models/file.go @@ -0,0 +1,18 @@ +package models + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +type File struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + Name string `bson:"name" json:"name"` + Path string `bson:"path" json:"path"` // Chemin physique du fichier sur le disque + Size int64 `bson:"size" json:"size"` + MimeType string `bson:"mime_type" json:"mime_type"` + FolderID primitive.ObjectID `bson:"folder_id" json:"folder_id"` + UserID primitive.ObjectID `bson:"user_id" json:"user_id"` + CreatedAt time.Time `bson:"created_at" json:"created_at"` + UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` +} diff --git a/app/back/internal/models/folder.go b/app/back/internal/models/folder.go new file mode 100644 index 0000000..e644d72 --- /dev/null +++ b/app/back/internal/models/folder.go @@ -0,0 +1,17 @@ +package models + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +type Folder struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + Name string `bson:"name" json:"name" binding:"required"` + Path string `bson:"path" json:"path"` // Chemin complet du dossier (ex: /user1/docs/images) + ParentID *primitive.ObjectID `bson:"parent_id,omitempty" json:"parent_id,omitempty"` + UserID primitive.ObjectID `bson:"user_id" json:"user_id"` + Depth int `bson:"depth" json:"depth"` // Profondeur dans l'arborescence (max 10) + CreatedAt time.Time `bson:"created_at" json:"created_at"` + UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` +} diff --git a/app/back/internal/models/user.go b/app/back/internal/models/user.go new file mode 100644 index 0000000..369b413 --- /dev/null +++ b/app/back/internal/models/user.go @@ -0,0 +1,19 @@ +package models + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +type User struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + Email string `bson:"email" json:"email"` + Password string `bson:"password" json:"password,omitempty"` // Permet la lecture mais pas le renvoi si vide + CreatedAt time.Time `bson:"created_at" json:"created_at"` + UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` +} + +type LoginRequest struct { + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required"` +} diff --git a/app/back/main.go b/app/back/main.go index 0537912..ca46d05 100644 --- a/app/back/main.go +++ b/app/back/main.go @@ -1,62 +1,109 @@ package main import ( + "app/internal/api" + "app/internal/handlers" + "app/internal/middleware" "context" + "log" + "net/http" + "os" + "time" + "github.com/gin-gonic/gin" "github.com/joho/godotenv" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - "log" - "os" ) -var mongoClient *mongo.Client - -func init() { +func main() { + // Chargement des variables d'environnement if err := godotenv.Load(); err != nil { log.Println("No .env file found") } -} -func main() { // Configuration MongoDB mongoURI := os.Getenv("MONGO_URI") if mongoURI == "" { - mongoURI = "mongodb://localhost:27017" + mongoURI = "mongodb://mongodb:27017" } // Connexion à MongoDB - clientOptions := options.Client().ApplyURI(mongoURI) - client, err := mongo.Connect(context.Background(), clientOptions) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI)) if err != nil { log.Fatal(err) } - defer client.Disconnect(context.Background()) + defer client.Disconnect(ctx) - // Vérification de la connexion - err = client.Ping(context.Background(), nil) - if err != nil { + // Ping de la base de données + if err := client.Ping(ctx, nil); err != nil { log.Fatal(err) } log.Println("Connected to MongoDB!") - mongoClient = client - // Configuration Gin + db := client.Database("goofy_cdn") + + // Initialisation des handlers + uploadDir := "./uploads" + if err := os.MkdirAll(uploadDir, 0755); err != nil { + log.Fatal(err) + } + + authHandler := handlers.NewAuthHandler(db) + fileHandler := handlers.NewFileHandler(db, uploadDir) + folderHandler := handlers.NewFolderHandler(db) + healthHandler := handlers.NewHealthHandler() + + // Configuration de Gin r := gin.Default() - // Routes - r.GET("/health", func(c *gin.Context) { - c.JSON(200, gin.H{ - "status": "ok", - }) - }) + // Ajouter le middleware CORS + r.Use(middleware.CORSMiddleware()) + + // Configuration des routes de test + api.SetupTestRoutes(r) + + // Routes publiques + r.POST("/register", authHandler.Register) + r.POST("/login", authHandler.Login) + + // Routes protégées + protected := r.Group("/api") + protected.Use(middleware.AuthMiddleware()) + { + // Gestion des dossiers + protected.GET("/folders", folderHandler.ListAllFolders) + protected.POST("/folders", folderHandler.CreateFolder) + protected.GET("/folders/:name", folderHandler.ListFolderContents) + protected.DELETE("/folders/:name", folderHandler.DeleteFolder) - // Démarrage du serveur + // Gestion des fichiers + protected.POST("/files", fileHandler.UploadFile) + protected.GET("/files/:id", fileHandler.GetFile) + protected.DELETE("/files/:id", fileHandler.DeleteFile) + } + + // Configuration du serveur HTTP port := os.Getenv("PORT") if port == "" { port = "8080" } - if err := r.Run(":" + port); err != nil { + + r.GET("/health", healthHandler.Health) + + server := &http.Server{ + Addr: "0.0.0.0:" + port, + Handler: r, + ReadTimeout: 30 * time.Second, // Augmente le timeout de lecture + WriteTimeout: 30 * time.Second, // Augmente le timeout d'écriture + IdleTimeout: 120 * time.Second, // Augmente le timeout d'inactivité + } + + log.Printf("Serveur démarré sur le port %s", port) + if err := server.ListenAndServe(); err != nil { log.Fatal(err) } } diff --git a/app/back/tmp/main b/app/back/tmp/main deleted file mode 100755 index 2f5f8c2..0000000 Binary files a/app/back/tmp/main and /dev/null differ diff --git a/app/back/uploads/67adb61a6601460bfc6b5d13/67adc98d64102aab751d8cb1.csv b/app/back/uploads/67adb61a6601460bfc6b5d13/67adc98d64102aab751d8cb1.csv new file mode 100644 index 0000000..c2fb087 --- /dev/null +++ b/app/back/uploads/67adb61a6601460bfc6b5d13/67adc98d64102aab751d8cb1.csv @@ -0,0 +1,1001 @@ +ARK;"Object name/Title";Author;Date;"Inventory number";Collection;"Geographic references / Places";Location;MNR +cl010000006;"moule d'orfèvre ; boucle d'oreille";;"-30 / 1952 (?) (époque islamique [?] ; époque romaine [?])";"E 11589";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000007;figurine;;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 14156; M. 4671";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000008;verrou;;"641 / 1952 (époque islamique)";"E 14055 B";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000010;"papyrus documentaire";;"600 / 699 (époque byzantine ; époque islamique)";"E 6673";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000011;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 6679";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000012;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 6679";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000013;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 6679";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000015;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 7143";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000016;"papyrus documentaire";;"-30 / 395 (époque romaine)";"E 3321";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000019;"vase ; verrerie ; avec contenu";;"641 / 1952 (époque islamique)";"E 25097 (?); E 25106 (?)";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000023;figurine;;"-1069 / -664 (Troisième Période intermédiaire)";"E 14264";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000028;châle;;"395 / 641 (époque byzantine)";"E 26118; AC 151; PH 105; C 50";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000029;vase;;"641 / 1952 (époque islamique)";"E 25288";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000030;figurine;;"395 / 641 (?) (époque byzantine [?])";"E 22337; MG 4838";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000031;châle;;"395 / 641 (époque byzantine)";"E 25277; X 4969; TC 863; I 112";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000032;tenture;;"395 / 641 (époque byzantine)";"E 25278; X 4965; TC 864";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000033;"châle ; tunique";;"395 / 641 (époque byzantine)";"E 25344; X 4966; TC 865";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000035;plaque;;"-30 / 395 (époque romaine)";"E 21513; MG 4014; E 29808";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000036;figurine;;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 20872; MG 3373";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000038;pendentif;;"-30 / 395 (?) (époque romaine [?])";"E 22864; Curtis n°365";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000039;perle;;"641 / 1952 (époque islamique)";"E 22957";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000040;"figurine ; sarcophage miniature";;"-30 / 395 (?) (époque romaine [?])";"E 14262; X 5410";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000041;"épingle à cheveux";;"-30 / 641 (?) (époque romaine [?] ; époque byzantine [?])";"E 14265; X 5485";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000042;figurine;;"-30 / 395 (?) (époque romaine [?] ; époque moderne [?])";"E 14358; X 5446";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000048;louche;;"-664 / 641 (?) (Basse Époque)";"N 2111 A";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000051;"décor de textile";;"395 / 641 (époque byzantine)";"E 26168; AC 201; PH 197; D 163";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000052;"décor de textile";;"395 / 641 (époque byzantine)";"E 26169; AC 202; PH 198; D 164";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000054;"tunique d'enfant";;"1000 / 1099 (Fatimides)";"E 26172; H 81; AC 205; PH 251";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000055;tenture;;"395 / 641 (époque byzantine)";"E 26173; AC 206; PH 302; C 29";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000056;"tabula ; clavus";;"395 / 641 (époque byzantine)";"E 26174; AC 207; PH 303; D 84";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000057;orbiculus;;"395 / 641 (époque byzantine)";"E 26175; AC 208; PH 304; D 99";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000058;orbiculus;;"395 / 641 (époque byzantine)";"E 26176; AC 209; PH 305; H 102";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000059;tabula;;"395 / 641 (époque byzantine)";"E 26177; AC 210; PH 306; E 52";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000060;tabula;;"395 / 641 (époque byzantine)";"E 26178; AC 211";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000061;tabula;;"395 / 641 (époque byzantine)";"E 26179; AC 212; PH 308; D 152";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000062;"plastron de tunique";;"395 / 641 (époque byzantine)";"E 26180; D 77; AC 213; PH 312";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000063;tabula;;"395 / 641 (époque byzantine)";"E 26182; AC 216; PH 326; D 89";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000064;tabula;;"395 / 641 (époque byzantine)";"E 26183; AC 217; PH 339; D 108";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000065;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26187; AC 221; PH 349; E 33";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000066;tabula;;"641 / 1171 (début époque islamique)";"E 26559; AC 593; PH 1896; H 145";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000067;"tunique ; clavus ; tabula";;"395 / 641 (époque byzantine)";"E 26561; AC 595; PH 1899; H 151";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000068;clavus;;"395 / 641 (époque byzantine)";"E 26562; AC 596; PH 1900; H 152";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000069;clavus;;"395 / 641 (époque byzantine)";"E 26563; AC 597; PH 1902; H 153";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000070;clavus;;"395 / 641 (époque byzantine)";"E 26564 A; AC 598; PH 2381; G 351";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000071;"clavus ; tabula";;"641 / 1171 (début époque islamique)";"E 26564 B; AC 598; PH 1904; H 154; E 26565 ERR";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000072;"manche d'habit ; bande de poignet";;"400 / 540 (époque byzantine)";"E 26566; AC 600; PH 1910; H 155";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000073;"bande décorative d'habillement ; tabula";;"395 / 641 (époque byzantine)";"E 26568; AC 602; PH 1914; H 156";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000074;clavus;;"395 / 641 (époque byzantine)";"E 26570; AC 604; PH 1917";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000075;orbiculus;;"395 / 641 (époque byzantine)";"E 26572; AC 606; PH 2403; G 300";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000076;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26575; AC 609; PH 1923; H 172";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000077;"plastron de tunique";;"395 / 641 (époque byzantine)";"E 26576; AC 610; PH 1924; H 173";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000078;clavus;;"395 / 641 (époque byzantine)";"E 26577; AC 611; PH 1925; H 174";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000079;"bande décorative d'habillement ; tabula";;"395 / 641 (époque byzantine)";"E 26578; H 175; AC 612; PH 1927";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000080;tabula;;"395 / 641 (époque byzantine)";"E 26579; AC 613; PH 1938; H 176";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000081;"perle prismatique";;"395 / 641 (?) (époque byzantine [?])";"E 11000";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000082;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26598; AC 632; PH 2420; F 103";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000083;tabula;;"395 / 641 (époque byzantine)";"E 26599; AC 633; PH 1961; H 178";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000084;orbiculus;;"395 / 641 (époque byzantine)";"E 26600; AC 634; PH 1962; I 12";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000085;clavus;;"395 / 641 (époque byzantine)";"E 26601; AC 635; PH 1963; I 1";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000086;clavus;;"395 / 641 (époque byzantine)";"E 26602; AC 636; PH 1966; I 2";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000087;"manche d'habit ; bande de poignet";;"395 / 641 (époque byzantine)";"E 26603; AC 637; PH 1968; I 3";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000088;clavus;;"395 / 641 (époque byzantine)";"E 26604; AC 638; PH 1969; I 13";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000089;clavus;;"395 / 641 (époque byzantine)";"E 26605; AC 639; PH 1975; I 5";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000090;clavus;;"395 / 641 (époque byzantine)";"E 26606; AC 640; PH 1978; I 6";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000091;clavus;;"395 / 641 (époque byzantine)";"E 26609; AC 643; PH 1991; I 11";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000092;"clavus ; tabula";;"395 / 641 (époque byzantine)";"E 26610; AC 644; PH 1993; I 9";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000093;clavus;;"395 / 641 (époque byzantine)";"E 26613; AC 647; PH 1999";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000094;clavus;;"395 / 641 (époque byzantine)";"E 26614; AC 648; PH 2007; H 114";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000095;orbiculus;;"395 / 641 (époque byzantine)";"E 26615; AC 649; PH 2008; F 76 et F 77";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000096;"étui à kohol";;"-664 / 641 (?) (Basse Époque)";"AF 6504";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000097;tabula;;"395 / 641 (époque byzantine)";"E 26580; AC 614; PH 1931; H 177";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000098;clavus;;"395 / 641 (époque byzantine)";"E 26582; AC 616; PH 1934; H 181";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000099;clavus;;"395 / 641 (époque byzantine)";"E 26583; AC 617; PH 2422; G 110";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000100;clavus;;"395 / 641 (époque byzantine)";"E 26584; AC 618; PH 2379; G 295";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000101;clavus;;"395 / 641 (époque byzantine)";"E 26585; AC 619; PH 2424; G 344";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000102;"tunique ; clavus ; tabula ; encolure";;"395 / 641 (époque byzantine)";"E 26586; AC 620; PH 1940; H 184";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000103;clavus;;"395 / 641 (époque byzantine)";"E 26587; AC 621; PH 2387; G 291";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000104;orbiculus;;"395 / 641 (époque byzantine)";"E 26588; AC 622; PH 2406; G 315";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000105;tabula;;"395 / 641 (époque byzantine)";"E 26589; AC 623; PH 1946; H 185";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000106;"clavus ; tabula";;"395 / 641 (époque byzantine)";"E 26591; AC 625; PH 1948; H 188";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000107;"manche d'habit ; bande de poignet";;"395 / 641 (époque byzantine)";"E 26592; AC 626; PH 1950; H 189";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000108;clavus;;"395 / 641 (époque byzantine)";"E 26593; AC 627; PH 1952; H 191";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000109;clavus;;"395 / 641 (époque byzantine)";"E 26596; AC 630; PH 1956; I 14";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000110;"clavus ; tabula";;"395 / 641 (époque byzantine)";"E 26597; AC 631; PH 1957; H 195";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000111;"tunique ; orbiculus";;"395 / 641 (époque byzantine)";"E 26616; AC 650; PH 2009; I 4";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000112;châle;;"395 / 641 (époque byzantine)";"E 26618; AC 652; PH 2054; PH 2001";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000113;orbiculus;;"395 / 641 (époque byzantine)";"E 26504; AC 538; PH 2398; F 100";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000114;"décor de textile";;"395 / 641 (époque byzantine)";"E 26505; AC 539; PH 1693; H 101";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000115;orbiculus;;"395 / 641 (époque byzantine)";"E 26506; AC 540; PH 2414; G 313";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000116;clavus;;"395 / 641 (époque byzantine)";"E 26507; AC 541; PH 1696; H 115";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000117;tabula;;"395 / 641 (époque byzantine)";"E 26512; AC 546; PH 1711; H 179";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000118;clavus;;"395 / 641 (époque byzantine)";"E 26513; AC 547; PH 1713; H 118";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000119;tabula;;"395 / 641 (époque byzantine)";"E 26514; AC 548; PH 1714; H 119";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000120;clavus;;"395 / 641 (époque byzantine)";"E 26515; AC 549; TC 2412; G 357";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000121;clavus;;"395 / 641 (époque byzantine)";"E 26516; AC 550; PH 2433; F 95";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000122;orbiculus;;"395 / 641 (époque byzantine)";"E 26519; AC 553; PH 2409; G 310";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000124;clavus;;"395 / 641 (époque byzantine)";"E 26393; AC 427; PH 1255; G 219";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000125;clavus;;"395 / 641 (époque byzantine)";"E 26394; AC 428; PH 1258; G 217";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000126;clavus;;"395 / 641 (époque byzantine)";"E 26395; AC 429; PH 2383; G 293";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000127;"tunique ; clavus";;"395 / 641 (époque byzantine)";"E 26397; AC 431; PH 1261; G 289";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000128;clavus;;"395 / 641 (époque byzantine)";"E 26398; AC 432; PH 1262; G 226";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000129;clavus;;"395 / 641 (époque byzantine)";"E 26401; AC 435; PH 1269; G 282";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000130;clavus;;"395 / 641 (époque byzantine)";"E 26403; AC 437; PH 1271; G 202";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000131;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26407; AC 441; PH 1351; H 9";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000132;"manche d'habit ; bande de poignet";;"395 / 641 (époque byzantine)";"E 26408; AC 442; PH 1352; H 10";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000133;clavus;;"395 / 641 (époque byzantine)";"E 26409; AC 443; PH 2441; G 288";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000134;clavus;;"395 / 641 (époque byzantine)";"E 26410; AC 444; PH 1354; H 11";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000135;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26411; AC 445; PH 1355; H 12";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000136;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26412; AC 446; PH 1356; H 13";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000137;orbiculus;;"395 / 641 (époque byzantine)";"E 26413; AC 447; PH 1357; H 14";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000138;orbiculus;;"395 / 641 (époque byzantine)";"E 26414; H 16; AC 448; PH 1360";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000139;orbiculus;;"395 / 641 (époque byzantine)";"E 26415; AC 449; PH 1361; H 17";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000140;tabula;;"395 / 641 (époque byzantine)";"E 26416; H 18; AC 450; PH 1363";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000141;orbiculus;;"395 / 641 (époque byzantine)";"E 26234; AC 268; PH 539; G 232";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000142;tunique;;"395 / 641 (époque byzantine)";"E 26238; AC 272; PH 558; F 204";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000143;tenture;;"395 / 641 (époque byzantine)";"E 26240; AC 274; PH 575; E 136";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000144;châle;;"395 / 641 (époque byzantine)";"E 26241; AC 275; PH 581; E 128";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000145;tenture;;"395 / 641 (époque byzantine)";"E 26242; AC 276; PH 582; E 135";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000146;tenture;;"395 / 641 (époque byzantine)";"E 26243; AC 277; PH 583; E 91";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000147;châle;;"395 / 641 (époque byzantine)";"E 26245; AC 279; PH 594; E 157";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000148;châle;;"395 / 641 (époque byzantine)";"E 26246; AC 280; PH 599; E 160";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000149;châle;;"395 / 641 (époque byzantine)";"E 26247; AC 281; PH 596; E 163";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000150;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26250; AC 284; PH 661; G 111";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000151;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26251; AC 285; PH 662; G 112";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000152;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26252; AC 286; PH 663; G 113";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000153;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26254; AC 288; PH 665; G 117";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000154;"décor de textile";;"395 / 641 (époque byzantine)";"E 26256; AC 290; PH 671; G 115";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000155;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26257; AC 291; PH 677; G 79";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000156;orbiculus;;"395 / 641 (époque byzantine)";"E 26258; AC 292; PH 678; G 80";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000157;orbiculus;;"395 / 641 (époque byzantine)";"E 26259; AC 293; PH 680; F 78";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000158;orbiculus;;"395 / 641 (époque byzantine)";"E 26260; AC 294; PH 681; G 81";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000159;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26357; AC 391; PH 1157; G 210";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000160;clavus;;"395 / 641 (époque byzantine)";"E 26358; AC 392; PH 1163; G 211";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000161;"décor de textile";;"395 / 641 (époque byzantine)";"E 26359; AC 393; PH 212; G 212";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000162;tissu;;"395 / 641 (époque byzantine)";"E 26360; AC 394; PH 1167; G 213";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000163;tissu;;"395 / 641 (époque byzantine)";"E 26361; AC 395; PH 1168; G 214";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000164;clavus;;"395 / 641 (époque byzantine)";"E 26362; G 215; AC 396; PH 1169";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000165;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26341; AC 375; PH 1127; G 180";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000166;clavus;;"395 / 641 (époque byzantine)";"E 26342; AC 376; PH 1129; G 181";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000167;clavus;;"395 / 641 (époque byzantine)";"E 26343; G 183; AC 377; PH 1130";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000168;clavus;;"395 / 641 (époque byzantine)";"E 26344; AC 378; PH 1132; G 184";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000169;clavus;;"395 / 641 (époque byzantine)";"E 26345; AC 379; PH 1137; G 198";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000170;clavus;;"395 / 641 (époque byzantine)";"E 26346; AC 380; PH 1139; G 196";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000171;clavus;;"395 / 641 (époque byzantine)";"E 26347; AC 381; PH 1140; G 197";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000172;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26350; AC 384; PH 1146; G 200";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000173;"manche d'habit ; bande de poignet";;"395 / 641 (époque byzantine)";"E 26351; AC 385; PH 1147; G 208";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000174;tabula;;"395 / 641 (époque byzantine)";"E 26190; AC 224; PH 353; F 218";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000175;orbiculus;;"395 / 641 (époque byzantine)";"E 26191; AC 225; PH 354; F 216";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000176;clavus;;"395 / 641 (époque byzantine)";"E 26192; AC 226; PH 356; G 106";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000177;"encolure ; clavus";;"395 / 641 (époque byzantine)";"E 26193; AC 227; PH 357; G 260";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000178;orbiculus;;"395 / 641 (époque byzantine)";"E 26200; AC 234; PH 374; F 217";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000179;clavus;;"395 / 641 (époque byzantine)";"E 26203; AC 237; PH 377; E 27";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000180;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26204; AC 238; PH 379; E 57";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000181;clavus;;"395 / 641 (époque byzantine)";"E 26206; AC 240; PH 406; F 5";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000182;clavus;;"395 / 641 (époque byzantine)";"E 26208; AC 242; PH 413; G 25";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000183;clavus;;"395 / 641 (époque byzantine)";"E 26209; AC 243; PH 415; E 90";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000184;"bande décorative d'habillement ; orbiculus";;"395 / 641 (époque byzantine)";"E 26210; AC 244; PH 418; H 1";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000185;"tunique ; clavus";;"395 / 641 (époque byzantine)";"E 26214; AC 248; PH 441; F 214";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000186;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26217; AC 251; PH 502; E 124";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000187;"manche d'habit ; bande de poignet";;"395 / 641 (époque byzantine)";"E 26218; AC 252; PH 503; F 195";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000188;tunique;;"395 / 641 (époque byzantine)";"E 26219; F 189; AC 253; PH 504";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000189;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26220; AC 254; PH 513; F 25";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000190;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26221; AC 255; PH 514; F 26";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000191;tenture;;"395 / 641 (époque byzantine)";"E 26224; AC 258; PH 521; G 274";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000192;"plastron de tunique";;"395 / 641 (époque byzantine)";"E 26225; AC 259; PH 522; G 257";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000193;clavus;;"395 / 641 (époque byzantine)";"E 26226; AC 260; PH 525; G 277";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000194;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26227; AC 261; PH 527; G 278";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000195;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26228; AC 262; PH 528; G 279";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000196;clavus;;"395 / 641 (époque byzantine)";"E 26230; AC 264; PH 530; G 281";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000197;clavus;;"395 / 641 (époque byzantine)";"E 26231; AC 265; PH 531; G 273";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000198;"manche d'habit ; bande de poignet";;"395 / 641 (époque byzantine)";"E 26232; AC 266; PH 535; G 230";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000199;"plastron de tunique";;"395 / 641 (époque byzantine)";"E 26233; AC 267; PH 536; G 231";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000200;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26363; AC 397; PH 1170; G 216";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000201;"épingle à cheveux ; calame ; bâtonnet";;"641 / 1952 (époque islamique)";"AF 1377";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000203;"épingle à cheveux";;"395 / 641 (époque byzantine)";"AF 917 bis";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000204;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26315; AC 349; PH 893; G 149";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000205;orbiculus;;"395 / 641 (époque byzantine)";"E 26317; AC 351; PH 897; F 53";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000206;clavus;;"395 / 641 (époque byzantine)";"E 26318; AC 352; PH 899; G 151";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000207;clavus;;"395 / 641 (époque byzantine)";"E 26319; AC 353; PH 900; G 156";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000208;orbiculus;;"395 / 641 (époque byzantine)";"E 26320; AC 354; PH 901; G 157";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000209;"décor de textile";;"395 / 641 (époque byzantine)";"E 26322; AC 356; PH 907; G 159";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000210;"plastron de tunique";;"395 / 641 (époque byzantine)";"E 26323; AC 357; PH 908; G 160";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000211;"tunique ; tabula ; bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26324; AC 358; PH 912; G 121";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000212;clavus;;"395 / 641 (époque byzantine)";"E 26325; AC 359; PH 914; G 122";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000213;orbiculus;;"395 / 641 (époque byzantine)";"E 26326; AC 360; PH 917; G 123";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000214;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26327; AC 361; PH 919; G 124";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000215;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26328; AC 362; PH 920; G 125";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000216;clavus;;"395 / 641 (époque byzantine)";"E 26329; AC 363; PH 922; G 126";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000217;orbiculus;;"395 / 641 (époque byzantine)";"E 26330; AC 364; PH 923; G 127";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000218;orbiculus;;"395 / 641 (époque byzantine)";"E 26331; AC 365; PH 924; G 128";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000219;"décor de textile";;"395 / 641 (époque byzantine)";"E 26332; AC 366; PH 926; G 129";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000220;orbiculus;;"395 / 641 (époque byzantine)";"E 26333; AC 367; PH 2413; G 312";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000221;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26334; AC 368; PH 938; G 130";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000222;"manche d'habit ; bande de poignet";;"395 / 641 (époque byzantine)";"E 26336; AC 370; PH 1117; G 177";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000223;clavus;;"395 / 641 (époque byzantine)";"E 26338; AC 372; PH 1122; G 175";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000224;clavus;;"395 / 641 (époque byzantine)";"E 26339; AC 373; PH 1123; G 176";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000225;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26340; AC 374; PH 1125; G 179";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000226;"étui à kohol";;"395 / 641 (?) (époque byzantine [?])";"E 24013";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000228;"épingle à cheveux";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 14266";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000229;cuiller;;"-30 / 284 (Haut Empire)";"E 15055";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000230;orbiculus;;"395 / 641 (époque byzantine)";"E 26262; AC 296; PH 683; G 83";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000231;tabula;;"395 / 641 (époque byzantine)";"E 26263; AC 297; PH 684; G 84";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000232;tabula;;"395 / 641 (époque byzantine)";"E 26264; AC 298; PH 685; G 85";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000233;tabula;;"395 / 641 (époque byzantine)";"E 26265; AC 299; PH 689; G 86";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000234;orbiculus;;"395 / 641 (époque byzantine)";"E 26266; AC 300; PH 690; G 88";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000235;orbiculus;;"395 / 641 (époque byzantine)";"E 26267; AC 301; PH 691; G 89";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000236;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26268; AC 302; PH 692; F 46";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000237;orbiculus;;"395 / 641 (époque byzantine)";"E 26269; AC 303; PH 694; G 91";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000238;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26270; AC 304; PH 696; G 92";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000239;orbiculus;;"395 / 641 (époque byzantine)";"E 26271; AC 305; PH 698; G 93";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000240;tabula;;"395 / 641 (époque byzantine)";"E 26272";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000241;"décor de textile";;"395 / 641 (époque byzantine)";"E 26274; AC 308; PH 703; G 96";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000242;orbiculus;;"395 / 641 (époque byzantine)";"E 26275; AC 309; PH 718; G 299";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000243;"décor de textile";;"395 / 641 (époque byzantine)";"E 26276; AC 310; PH 720; H 4";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000244;"décor de textile";;"395 / 641 (époque byzantine)";"E 26277; AC 311; PH 721; H 5";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000245;clavus;;"395 / 641 (époque byzantine)";"E 26278; AC 312; PH 726; G 118";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000246;"manche d'habit ; bande de poignet";;"395 / 641 (époque byzantine)";"E 26279; AC 313; PH 727; G 119";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000247;orbiculus;;"395 / 641 (époque byzantine)";"E 26280; AC 314; PH 734; G 223";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000248;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26281; AC 315; PH 740; H 7";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000249;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26282; AC 316; PH 741; G 145";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000250;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26283; AC 317; PH 742; H 8";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000251;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26284; AC 318; PH 746; G 120";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000252;"décor de textile";;"395 / 641 (époque byzantine)";"E 26286; AC 320; PH 750; G 138";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000253;"décor de textile";;"395 / 641 (époque byzantine)";"E 26287; AC 321; PH 759; G 139";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000254;orbiculus;;"395 / 641 (époque byzantine)";"E 26288; AC 322; PH 764; G 103";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000255;orbiculus;;"395 / 641 (époque byzantine)";"E 26289; AC 323; PH 765; G 104";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000256;clavus;;"395 / 641 (époque byzantine)";"E 26290; AC 324; PH 767; G 107";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000257;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26291; AC 325; PH 770; G 108";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000258;tabula;;"395 / 641 (époque byzantine)";"E 26292; AC 326; PH 772; G 109";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000259;"clavus ; orbiculus";;"395 / 641 (époque byzantine)";"E 26293; AC 327; PH 773; F 51";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000260;clavus;;"395 / 641 (époque byzantine)";"E 26294; AC 328; PH 775; F 48";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000261;clavus;;"395 / 641 (époque byzantine)";"E 26295; AC 329; PH 779; F 213";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000262;"plastron de tunique";;"395 / 641 (époque byzantine)";"E 26302; AC 336; PH 851; G 203";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000263;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26303; AC 337; PH 853; G 140";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000264;orbiculus;;"395 / 641 (époque byzantine)";"E 26304; AC 338; PH 861; G 235";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000265;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26305; AC 339; PH 866; G 141";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000266;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26306; AC 340; PH 867; D 153";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000267;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26307; AC 341; PH 876; G 143";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000268;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26309; AC 343; PH 880; G 144";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000269;"bande d'encolure";;"395 / 641 (époque byzantine)";"E 26310; AC 344; PH 882; G 146";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000270;"décor de textile";;"395 / 641 (époque byzantine)";"E 26311; AC 345; PH 888; G 153";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000271;"décor de textile";;"395 / 641 (époque byzantine)";"E 26313; AC 347; PH 891; G 155";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000272;tenture;;"395 / 641 (époque byzantine)";"E 26460; AC 494; PH 1423; H 67";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000273;tenture;;"395 / 641 (époque byzantine)";"E 26462; AC 496; PH 1430; H 70";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000274;tenture;;"395 / 641 (époque byzantine)";"E 26463; AC 497; PH 1431; H 71";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000275;tabula;;"395 / 641 (époque byzantine)";"E 26465; AC 499; PH 1433; H 73";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000276;"plastron de tunique";;"395 / 641 (époque byzantine)";"E 26468; AC 502; PH 1437; H 77";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000277;"décor de textile";;"395 / 641 (époque byzantine)";"E 26469; AC 503; PH 1438; H 78";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000278;"décor de textile";;"395 / 641 (époque byzantine)";"E 26470; AC 504; PH 1439; H 79";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000279;tabula;;"395 / 641 (époque byzantine)";"E 26472; AC 506; PH 1442; H 196";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000280;tenture;;"395 / 641 (époque byzantine)";"E 26473; AC 507; PH 1447; H 82";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000281;"exercice de tissage";;"880 / 1020 (époque islamique)";"E 26474; AC 508; PH 1448; H 83";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000282;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26475; AC 509; PH 1452; G 218";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000283;"exercice de tissage";;"880 / 1020 (époque islamique)";"E 26476; AC 510; PH 1459; H 84";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000284;"exercice de tissage";;"880 / 1020 (époque islamique)";"E 26477; AC 511; PH 1466; H 85";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000285;"exercice de tissage";;"880 / 1020 (époque islamique)";"E 26478; AC 512; PH 1468; H 86";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000286;"exercice de tissage";;"880 / 1020 (époque islamique)";"E 26479; AC 513; PH 1472; H 87";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000287;"exercice de tissage";;"880 / 1020 (époque islamique)";"E 26480; AC 514; PH 1473; H 88";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000288;"décor de textile";;"395 / 641 (époque byzantine)";"E 26481; AC 515; PH 1476; H 89";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000289;clavus;;"395 / 641 (époque byzantine)";"E 26482; AC 516; PH 1481; H 90";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000290;clavus;;"395 / 641 (époque byzantine)";"E 26486; AC 520; PH 1603; G 58";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000291;"décor de textile";;"395 / 641 (époque byzantine)";"E 26487; AC 521; PH 1610; H 93";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000292;tabula;;"395 / 641 (époque byzantine)";"E 26488; AC 522; PH 1616; H 117";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000293;"décor de textile";;"395 / 641 (époque byzantine)";"E 26493; AC 527; PH 1636; H 94";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000294;tabula;;"395 / 641 (époque byzantine)";"E 26494; AC 528; PH 1650; H 95";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000295;tabula;;"395 / 641 (époque byzantine)";"E 26497; AC 531; PH 1664; H 104";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000296;clavus;;"395 / 641 (époque byzantine)";"E 26499; AC 533; PH 1680; H 92";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000297;clavus;;"395 / 641 (époque byzantine)";"E 26500; AC 534; PH 1682; G 62";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000298;clavus;;"395 / 641 (époque byzantine)";"E 26501; AC 535; PH 1684; G 63";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000299;orbiculus;;"395 / 641 (époque byzantine)";"E 26503; AC 537; PH 2408; G 311";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000300;tissu;;"395 / 641 (époque byzantine)";"E 26110; AC 143; A 14; PH 7";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000301;"châle ; couverture";;"170 / 400 (époque byzantine)";"E 26124; AC 157; PH 114; B 5";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000302;"manche d'habit ; bande de poignet";;"395 / 641 (époque byzantine)";"E 26125; AC 158; PH 115; C 55";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000303;tabula;;"395 / 641 (époque byzantine)";"E 26128; AC 161; PH 120; I 18";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000304;"plastron de tunique";;"641 / 1171 (début époque islamique)";"E 26132; PH 127; D 96; AC 165; D 78.5.16";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000306;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26135; AC 168; PH 132; D 3";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000307;"châle ; couverture";;"395 / 641 (époque byzantine)";"E 26137; AC 170; PH 137; D 93";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000308;clavus;;"395 / 641 (époque byzantine)";"E 26138; AC 171; PH 138; D 116";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000309;clavus;;"395 / 641 (époque byzantine)";"E 26139; AC 172; PH 139; D 117";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000310;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26149; AC 182; PH 156; C 56";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000311;châle;;"395 / 641 (époque byzantine)";"E 26151; AC 184; PH 158; A 21; A 21 bis; PH 160";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000312;châle;;"210 / 390 (époque byzantine ; époque romaine)";"E 26152; AC 185; PH 159; A 22";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000313;clavus;;"395 / 641 (époque byzantine)";"E 26160; AC 193; PH 171; D 14";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000314;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26161; AC 194; PH 177; D 23";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000315;"bande décorative d'habillement ; tabula";;"395 / 641 (époque byzantine)";"E 26162; AC 195; PH 178; E 64";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000316;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26163; AC 196";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000317;"orbiculus ; bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26189; AC 223; PH 352; G 255";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000318;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26364; AC 398; PH 1171; G 209";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000319;clavus;;"395 / 641 (époque byzantine)";"E 26365; AC 399; PH 1173; G 238";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000320;orbiculus;;"395 / 641 (époque byzantine)";"E 26367; AC 401; PH 2397; G 309";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000321;châle;;"395 / 641 (époque byzantine)";"E 26368; AC 402; PH 1181; G 239";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000322;orbiculus;;"395 / 641 (époque byzantine)";"E 26369; AC 403; PH 2395; G 307";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000323;orbiculus;;"395 / 641 (époque byzantine)";"E 26372; AC 406; PH 2394; G 306";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000324;orbiculus;;"395 / 641 (époque byzantine)";"E 26373; AC 407; PH 2400; F 98";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000325;"bande d'encolure";;"395 / 641 (époque byzantine)";"E 26374; AC 408; PH 1202; G 241";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000326;clavus;;"395 / 641 (époque byzantine)";"E 26375; AC 409; PH 1203; G 242";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000327;clavus;;"395 / 641 (époque byzantine)";"E 26377; AC 411; PH 1215; G 244";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000328;tabula;;"395 / 641 (époque byzantine)";"E 26378; AC 412; PH 1214; G 243";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000329;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26379; AC 413; PH 1219; G 245";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000330;"décor de textile";;"395 / 641 (époque byzantine)";"E 26380; AC 414; PH 1224; G 246";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000331;clavus;;"395 / 641 (époque byzantine)";"E 26381; AC 415; PH 2421; F 101";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000332;orbiculus;;"395 / 641 (époque byzantine)";"E 26382; AC 416; PH 2396; G 308";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000333;"tunique ; bande de poignet ; orbiculus";;"395 / 641 (époque byzantine)";"E 26383; AC 417; PH 1228; G 247";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000334;orbiculus;;"395 / 641 (époque byzantine)";"E 26384; AC 418; PH 1230; G 418";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000335;"plastron de tunique";;"395 / 641 (époque byzantine)";"E 26385; AC 419; PH 1231; G 419";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000336;"tabula ; bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26386; AC 420; PH 1241; G 284";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000337;"capuche ; fragment";;"époque byzantine";"E 26387; AC 421; PH 1244; E 34";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000338;orbiculus;;"395 / 641 (époque byzantine)";"E 26388; AC 422; PH 2389; G 292";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000339;orbiculus;;"395 / 641 (époque byzantine)";"E 26389; AC 423; PH 1247; G 225";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000340;tabula;;"395 / 641 (époque byzantine)";"E 26417; AC 451; PH 1364; H 19";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000341;orbiculus;;"395 / 641 (époque byzantine)";"E 26418; AC 452; PH 1365; H 20";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000342;clavus;;"395 / 641 (époque byzantine)";"E 26419; AC 453; PH 1366; H 21";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000343;"plastron de tunique";;"395 / 641 (époque byzantine)";"E 26420; AC 454; PH 1367; H 22";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000344;"plastron de tunique";;"395 / 641 (époque byzantine)";"E 26421; AC 455; PH 1368; H 23";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000345;"plastron de tunique";;"395 / 641 (époque byzantine)";"E 26422; AC 456; PH 1369; H 24";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000346;tabula;;"395 / 641 (époque byzantine)";"E 26423; AC 457; PH 1370; H 25";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000347;tabula;;"395 / 641 (époque byzantine)";"E 26424; AC 458; PH 1371; H 26";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000348;"décor de textile";;"395 / 641 (époque byzantine)";"E 26425; AC 459; PH 1372; H 27";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000349;orbiculus;;"395 / 641 (époque byzantine)";"E 26426; AC 460; PH 1373; H 28";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000350;orbiculus;;"395 / 641 (époque byzantine)";"E 26427; AC 461; PH 1374; H 29";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000351;clavus;;"395 / 641 (époque byzantine)";"E 26428; AC 462; PH 1375; H 30";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000352;orbiculus;;"395 / 641 (époque byzantine)";"E 26429; AC 463; PH 1376; H 31";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000353;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26430; AC 464; PH 1377; H 32";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000354;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26431; AC 465; PH 1379; H 33";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000355;clavus;;"395 / 641 (époque byzantine)";"E 26432; AC 466; PH 1380; H 34";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000356;clavus;;"395 / 641 (époque byzantine)";"E 26433; AC 467; PH 1381; H 35";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000357;"décor de textile";;"395 / 641 (époque byzantine)";"E 26434; AC 468; PH 1382; H 36";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000358;"plastron de tunique";;"395 / 641 (époque byzantine)";"E 26435; AC 469; PH 1383; H 37";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000359;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26437; AC 471; PH 1385; H 40";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000360;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26438; AC 472; PH 1386; H 41";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000361;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26439; AC 473; PH 1388; H 42";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000362;"décor de textile";;"395 / 641 (époque byzantine)";"E 26120";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000363;clavus;;"395 / 641 (époque byzantine)";"E 26441; H 44; AC 475; PH 1390";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000364;"bande de poignet";;"641 / 1171 (début époque islamique)";"E 26442; AC 476; PH 1393; H 45";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000365;orbiculus;;"641 / 1171 (début époque islamique)";"E 26444; AC 478; PH 1396; H 47";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000366;tenture;;"395 / 641 (époque byzantine)";"E 26445; AC 479; PH 1397; H 49";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000367;"décor de textile";;"395 / 641 (époque byzantine)";"E 26446; AC 480; PH 1399; H 50";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000368;"plastron de tunique";;"395 / 641 (époque byzantine)";"E 26447; AC 481; PH 1403; H 51";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000369;tabula;;"395 / 641 (époque byzantine)";"E 26448; AC 482; PH 1404; H 52";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000370;tenture;;"395 / 641 (époque byzantine)";"E 26449; AC 483; PH 1405; H 53";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000371;tenture;;"395 / 641 (époque byzantine)";"E 26450; AC 484; PH 1406; H 54";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000372;tabula;;"395 / 641 (époque byzantine)";"E 26451; AC 485; PH 1409; H 55";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000373;"décor de textile";;"395 / 641 (époque byzantine)";"E 26452; AC 486; PH 1410; H 57";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000374;"plastron de tunique";;"395 / 641 (époque byzantine)";"E 26453; AC 487; PH 1414; H 58";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000375;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26454; AC 488; PH 1415; H 59";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000376;"décor de textile";;"395 / 641 (époque byzantine)";"E 26455; AC 489; PH 1416; H 60";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000377;tabula;;"395 / 641 (époque byzantine)";"E 26520; AC 554; PH 1729; H 120";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000378;tabula;;"395 / 641 (époque byzantine)";"E 26522; AC 556; PH 1733; H 121";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000379;tabula;;"395 / 641 (époque byzantine)";"E 26523; AC 557; PH 1735; H 147";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000380;châle;;"395 / 641 (époque byzantine)";"E 26526 A; AC 560; PH 1804; I 10";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000381;"tunique ; plastron de tunique ; clavus ; tabula";;"395 / 641 (époque byzantine)";"E 26530; AC 564; PH 1808; H 108";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000382;"bande de poignet";;"641 / 1171 (début époque islamique)";"E 26531; AC 565; PH 1851; H 122";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000383;clavus;;"641 / 1171 (début époque islamique)";"E 26533; AC 567; PH 1853; H 124";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000384;orbiculus;;"395 / 641 (époque byzantine)";"E 26534; AC 568; PH 2405; G 302";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000385;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26538; AC 572; PH 1859; H 129";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000386;"bande décorative d'habillement ; tabula";;"395 / 641 (époque byzantine)";"E 26539; AC 573; PH 1860; H 130";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000387;tabula;;"395 / 641 (époque byzantine)";"E 26541; AC 575; PH 1865; H 132";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000388;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26542; AC 576; PH 1866; H 38";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000389;clavus;;"395 / 641 (époque byzantine)";"E 26544; AC 578; PH 1869; H 134";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000390;"clavus ; tabula";;"395 / 641 (époque byzantine)";"E 26545; AC 579; PH 1871; H 135";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000391;"plastron de tunique ; clavus ; tabula";;"641 / 1171 (début époque islamique)";"E 26546; AC 580; PH 1872; H 136";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000392;clavus;;"395 / 641 (époque byzantine)";"E 26548; AC 582; PH 1876; H 139";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000393;"tunique ; tabula";;"395 / 641 (époque byzantine)";"E 26549; AC 583; PH 1877; H 140";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000394;tabula;;"395 / 641 (époque byzantine)";"E 26550; AC 584; PH 1882; I 15";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000395;tabula;;"395 / 641 (époque byzantine)";"E 26551; AC 585; PH 1883; H 141";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000396;tabula;;"395 / 641 (époque byzantine)";"E 26552; AC 586; PH 2402; F 97";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000397;tabula;;"395 / 641 (époque byzantine)";"E 26553; AC 587; PH 1886; H 142";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000398;tabula;;"395 / 641 (époque byzantine)";"E 26554; AC 588; PH 1888; H 143";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000399;tabula;;"395 / 641 (époque byzantine)";"E 26555; AC 589; PH 1889; H 144";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000400;tabula;;"395 / 641 (époque byzantine)";"E 26556; AC 590; PH 1891; H 146";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000401;tabula;;"395 / 641 (époque byzantine)";"E 26557; AC 591; PH 1893; H 148";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000402;tabula;;"395 / 641 (époque byzantine)";"E 26558; AC 592; PH 1894; H 149";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000403;"tunique ; encolure ; clavus";;"395 / 641 (époque byzantine)";"E 26619; AC 653; PH 2056; PH 2004";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000404;clavus;;"395 / 641 (époque byzantine)";"E 26622; AC 656; PH 2419; G 32";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000405;clavus;;"641 / 1171 (début époque islamique)";"E 26623; AC 657; PH 2103; I 23";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000406;clavus;;"641 / 1171 (début époque islamique)";"E 26625; AC 659; PH 2105; PH 2035";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000407;clavus;;"641 / 1171 (début époque islamique)";"E 26626; AC 660; PH 2106; I 25";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000408;clavus;;"641 / 1171 (début époque islamique)";"E 26628; AC 662; PH 2108; I 27";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000409;orbiculus;;"395 / 641 (époque byzantine)";"E 26629; AC 663; PH 2431; F 93";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000410;clavus;;"641 / 1171 (début époque islamique)";"E 26630; AC 664; PH 2110; I 28";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000411;clavus;;"641 / 1171 (début époque islamique)";"E 26631; AC 665; PH 2111; I 29";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000412;orbiculus;;"395 / 641 (époque byzantine)";"E 26632; AC 666; PH 2429; E 152";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000413;clavus;;"641 / 1171 (début époque islamique)";"E 26633; AC 667; PH 2113; I 34";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000414;clavus;;"641 / 1171 (début époque islamique)";"E 26634; AC 668; PH 2114; I 32";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000415;clavus;;"395 / 641 (époque byzantine)";"E 26635; AC 669; PH 2384; F 69";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000416;clavus;;"395 / 641 (époque byzantine)";"E 26636; AC 670; PH 2116; I 117";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000417;clavus;;"395 / 641 (époque byzantine)";"E 26637; AC 671; PH 2442; G 29";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000418;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26638; PH 2445; G 379";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000419;clavus;;"395 / 641 (époque byzantine)";"E 26639; AC 673; PH 2416; G 27";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000420;clavus;;"395 / 641 (époque byzantine)";"E 26640; AC 674; PH 2415; F 65";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000421;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26641; AC 675; PH 2444; G 318";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000422;clavus;;"641 / 1171 (début époque islamique)";"E 26642; AC 676; PH 2122; I 33";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000423;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26643; AC 677; PH 2418; G 4";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000424;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26644; AC 678; PH 2124; I 119";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000425;orbiculus;;"395 / 641 (époque byzantine)";"E 26645; AC 679; PH 2428; F 96";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000426;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26646; AC 680; PH 2126; I 35";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000427;clavus;;"395 / 641 (époque byzantine)";"E 26647; AC 681; PH 2127; I 36";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000428;clavus;;"641 / 1171 (début époque islamique)";"E 26648; PH 2128; I 37; AC 682";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000429;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26649; AC 683; PH 2129; I 38";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000430;clavus;;"641 / 1171 (début époque islamique)";"E 26650; AC 684; PH 2130; I 39";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000431;"clavus ; tabula";;"641 / 1171 (début époque islamique)";"E 26651; AC 685; PH 2131; I 24";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000432;clavus;;"641 / 1171 (début époque islamique)";"E 26652; AC 686; PH 2132; I 40";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000433;"clavus ; tabula";;"641 / 1171 (début époque islamique)";"E 26653; I 41; AC 687; PH 2133";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000434;orbiculus;;"395 / 641 (époque byzantine)";"E 26654; AC 688; PH 2435; G 360";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000435;"manche d'habit ; bande de poignet";;"641 / 1171 (début époque islamique)";"E 26655; AC 689; PH 2135; I 42";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000436;"manche d'habit ; bande de poignet";;"395 / 641 (époque byzantine)";"E 26656; AC 690; PH 2136; I 44";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000437;orbiculus;;"395 / 641 (époque byzantine)";"E 26657; AC 691; F 94; PH 2432";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000438;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26658; AC 692; PH 2451; F 6";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000439;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26659; G 7; AC 693; PH 2448";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000440;"manche d'habit ; bande de poignet";;"641 / 1171 (début époque islamique)";"E 26660; AC 694; PH 2140; I 45";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000441;"manche d'habit ; bande de poignet";;"395 / 641 (époque byzantine)";"E 26661; I 46; AC 695; PH 2141";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000442;"bande de poignet";;"641 / 1171 (début époque islamique)";"E 26662; AC 696; PH 2142; I 48";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000443;"bande de poignet";;"641 / 1171 (début époque islamique)";"E 26663; AC 697; PH 2143; I 47";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000444;"plastron de tunique";;"641 / 1171 (début époque islamique)";"E 26665; AC 699; PH 2145; I 17";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000445;"manche d'habit ; bande de poignet";;"395 / 641 (époque byzantine)";"E 26667; AC 701; PH 2147; I 51";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000446;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26668; AC 702; PH 2148; I 52";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000447;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26669; AC 703; PH 2149; I 53";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000448;clavus;;"641 / 1171 (début époque islamique)";"E 26672; AC 706; PH 2152; I 54";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000449;clavus;;"641 / 1171 (début époque islamique)";"E 26673; AC 707; PH 2153; I 55";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000450;clavus;;"641 / 1171 (début époque islamique)";"E 26674; AC 708; PH 2154; I 56";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000451;clavus;;"641 / 1171 (début époque islamique)";"E 26675; AC 709; PH 2155; PH 2089; I 57";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000452;clavus;;"641 / 1171 (début époque islamique)";"E 26676; AC 710; PH 2156; I 64";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000453;clavus;;"641 / 1171 (début époque islamique)";"E 26677; AC 711; PH 2157; I 65";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000454;clavus;;"641 / 1171 (début époque islamique)";"E 26678; AC 712; PH 2158; I 66";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000455;clavus;;"641 / 1171 (début époque islamique)";"E 26679; AC 713; PH 2159; I 67";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000456;clavus;;"641 / 1171 (début époque islamique)";"E 26680; AC 714; PH 2160; i 68";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000457;clavus;;"641 / 1171 (début époque islamique)";"E 26681; AC 715; PH 2161; I 69";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000458;clavus;;"641 / 1171 (début époque islamique)";"E 26682; AC 716; PH 2162; I 70";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000459;clavus;;"641 / 1171 (début époque islamique)";"E 26683; AC 717; PH 2163; I 71";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000460;tabula;;"641 / 1171 (début époque islamique)";"E 26684; AC 718; PH 2164; I 74";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000461;tabula;;"641 / 1171 (début époque islamique)";"E 26686; AC 720; PH 2166; I 77";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000462;tabula;;"641 / 1171 (début époque islamique)";"E 26687; PH 2167; I 78; AC 721";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000463;tabula;;"641 / 1171 (début époque islamique)";"E 26688; AC 722; PH 2168; I 80";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000464;tabula;;"641 / 1171 (début époque islamique)";"E 26689; AC 723; PH 2169; I 81";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000465;tabula;;"641 / 1171 (début époque islamique)";"E 26690; AC 724; PH 2170; I 82";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000466;tabula;;"641 / 1171 (début époque islamique)";"E 26691; AC 725; PH 2171; I 83";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000467;tabula;;"641 / 1171 (début époque islamique)";"E 26693; AC 727; PH 2173; I 84";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000468;tabula;;"641 / 1171 (début époque islamique)";"E 26694; AC 728; PH 2174; I 85";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000469;tabula;;"641 / 1171 (début époque islamique)";"E 26695; AC 729; PH 2175; I 86";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000470;tabula;;"641 / 1171 (début époque islamique)";"E 26696; AC 730; PH 2176; I 88";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000471;tabula;;"641 / 1171 (début époque islamique)";"E 26697; AC 731; PH 2177; I 89";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000472;"tunique ; tabula";;"395 / 641 (époque byzantine)";"E 26698; AC 732; PH 2178; I 90";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000473;tabula;;"641 / 1171 (début époque islamique)";"E 26699; AC 733; PH 2179; I 91";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000474;orbiculus;;"395 / 641 (époque byzantine)";"E 26700; AC 734; PH 2180; I 92";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000475;clavus;;"641 / 1171 (début époque islamique)";"E 26701; AC 735; PH 2181; I 31";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000476;"tunique ; tabula";;"395 / 641 (époque byzantine)";"E 26702; AC 736; PH 2182; I 93";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000477;tabula;;"641 / 1171 (début époque islamique)";"E 26703; AC 737; PH 2183; I 94";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000478;tabula;;"641 / 1171 (début époque islamique)";"E 26704; AC 738; PH 2184; I 95; PH 2124";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000479;tabula;;"641 / 1171 (début époque islamique)";"E 26706; AC 740; PH 2186; I 97";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000480;tabula;;"641 / 1171 (début époque islamique)";"E 26707; AC 741; PH 2187; I 98";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000481;orbiculus;;"395 / 641 (époque byzantine)";"E 26708; AC 742; PH 2188; I 99";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000482;tabula;;"641 / 1171 (début époque islamique)";"E 26710; AC 744; PH 2190; I 100";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000483;tabula;;"641 / 1171 (début époque islamique)";"E 26711; AC 745; PH 2191; I 101";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000484;tabula;;"641 / 1171 (début époque islamique)";"E 26712; AC 746; PH 2192; I 102";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000485;tabula;;"641 / 1171 (début époque islamique)";"E 26713; AC 747; PH 2193; I 103; PH 2124";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000486;tabula;;"641 / 1171 (début époque islamique)";"E 26714; AC 748; PH 2194; I 104";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000487;tabula;;"641 / 1171 (début époque islamique)";"E 26715; AC 749; PH 2195; I 105";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000488;tabula;;"641 / 1171 (début époque islamique)";"E 26716; AC 750; PH 2196; I 106";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000489;tabula;;"641 / 1171 (début époque islamique)";"E 26718; AC 752; PH 2198; I 107";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000490;orbiculus;;"395 / 641 (époque byzantine)";"E 26720; AC 754; PH 2201";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000491;tabula;;"641 / 1171 (début époque islamique)";"E 26721; AC 755; PH 2202; I 108";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000492;orbiculus;;"395 / 641 (époque byzantine)";"E 26722; AC 756; PH 2203; I 109";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000493;"bande décorative d'habillement";;"641 / 1171 (début époque islamique)";"E 26723";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000494;clavus;;"641 / 1171 (début époque islamique)";"E 26724; AC 758; PH 2207; I 111";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000495;"décor de textile";;"395 / 641 (époque byzantine)";"E 26730; AC 764; PH 2227; I 113";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000496;orbiculus;;"641 / 1171 (début époque islamique)";"E 26732; AC 766; PH 2231; I 114";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000497;orbiculus;;"641 / 1171 (début époque islamique)";"E 26733; AC 767; PH 2232; I 115";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000498;"tunique ; orbiculus";;"395 / 641 (époque byzantine)";"E 26734; AC 768; PH 2237; I 116";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000499;"tunique ; clavus ; orbiculus";;"395 / 641 (époque byzantine)";"E 26739; PH 2304; AC 773; F 75";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000500;"tunique ; clavus";;"395 / 641 (époque byzantine)";"E 26740; PH 2351; AC 774; G 134";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000501;clavus;;"395 / 641 (époque byzantine)";"E 26741; AC 775; PH 2353; G 135";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000502;clavus;;"395 / 641 (époque byzantine)";"E 26742; AC 776; PH 2358; G 285";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000503;clavus;;"395 / 641 (époque byzantine)";"E 26745; AC 779; PH 2361; G 354";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000504;clavus;;"395 / 641 (époque byzantine)";"E 26746; AC 780; PH 2363; G 355";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000505;orbiculus;;"395 / 641 (époque byzantine)";"E 26750; AC 784; PH 2373; F 160";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000506;clavus;;"395 / 641 (époque byzantine)";"E 26751; AC 785; PH 2375; F 66";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000507;clavus;;"395 / 641 (époque byzantine)";"E 26752; AC 786; PH 2377; G 296";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000508;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26753; AC 787; PH 2453; G 26";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000509;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26754; AC 788; PH 2454; F 70";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000510;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26755; AC 789; PH 2470; F 129";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000511;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26756; AC 790; PH 2473; F 130";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000512;clavus;;"395 / 641 (époque byzantine)";"E 26757; AC 791; PH 2475; F 9";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000513;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26758; AC 792; PH 2478; F 3";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000514;tabula;;"395 / 641 (époque byzantine)";"E 26759; AC 793; PH 2479; F 123";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000515;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26762; AC 796; PH 2484; F 185";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000516;"décor de textile";;"395 / 641 (époque byzantine)";"E 26763; AC 797; PH 2488; E 101";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000517;clavus;;"395 / 641 (époque byzantine)";"E 26765; AC 799; PH 2491; F 161";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000518;orbiculus;;"395 / 641 (époque byzantine)";"E 26766; AC 800; PH 2492; F 156";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000519;orbiculus;;"395 / 641 (époque byzantine)";"E 26767; AC 801; PH 2493; F 157";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000520;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26770; AC 804; PH 2513; F 158";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000521;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26771; AC 805; PH 2514; G 132";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000522;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26772; AC 806; PH 2523; G 11";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000523;orbiculus;;"395 / 641 (époque byzantine)";"E 26773; AC 807; PH 2529; F 72";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000524;orbiculus;;"395 / 641 (époque byzantine)";"E 26774; AC 808; PH 2530; F 74";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000525;orbiculus;;"395 / 641 (époque byzantine)";"E 26775; AC 809; PH 2531; F 71";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000526;orbiculus;;"395 / 641 (époque byzantine)";"E 26776; AC 810; PH 2538; F 79";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000527;tabula;;"641 / 1171 (début époque islamique)";"E 26777; AC 811; PH 2539; F 80";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000528;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26778; AC 812; PH 2541; F 81";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000529;tabula;;"395 / 641 (époque byzantine)";"E 26779; AC 813; PH 2543; G 36";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000530;tabula;;"395 / 641 (époque byzantine)";"E 26780; AC 814; PH 2544; G 37";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000531;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26781; AC 815; PH 2545; G 35";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000532;clavus;;"395 / 641 (époque byzantine)";"E 26783; AC 817; PH 2554; F 87";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000533;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26784; AC 818; PH 2556; F 88";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000534;orbiculus;;"395 / 641 (époque byzantine)";"E 26785; AC 819; PH 2557; F 89";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000535;"textile d'ameublement";;"395 / 641 (époque byzantine)";"E 26786; AC 820; PH 2563; F 82";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000536;tabula;;"395 / 641 (époque byzantine)";"E 26787; AC 821; PH 2555; F 83";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000537;"bande de poignet";;"395 / 641 (époque byzantine)";"E 26788; AC 822; PH 2566; F 84";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000538;clavus;;"641 / 1171 (début époque islamique)";"E 26789; AC 823; PH 2569; F 86";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000539;tenture;;"395 / 641 (époque byzantine)";"E 26795; AC 829; L 4; PH 2606";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000540;capuche;;"900 / 1099 (époque islamique)";"E 26799; AC 833; L 9";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000541;"manche d'habit ; bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26800; AC 834; PH 2708; F 67";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000542;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26802; AC 836; PH 2715; F 105";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000543;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26804; AC 838; PH 2719; F 106";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000544;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26805; AC 839; PH 2721; F 107";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000545;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26809; AC 843; PH 2730; F 112";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000546;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26810; AC 844; PH 2735; F 113";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000547;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26811; F 114; AC 845; PH 2737";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000548;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 26814; AC 848; PH 2745; F 119";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000551;"moulage ; arc";;"1750 / 1950 (époque moderne)";"E 26827 a; AC 934";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000552;orbiculus;;"395 / 641 (époque byzantine)";"E 29008; Guimet 936";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000553;tabula;;"395 / 641 (époque byzantine)";"E 29009; Guimet 937";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000560;orbiculus;;"395 / 641 (époque byzantine)";"E 29016; Guimet 944";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000565;orbiculus;;"395 / 641 (époque byzantine)";"E 29021; Guimet 949";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000566;"bande d'encolure";;"395 / 641 (époque byzantine)";"E 29022; Guimet 950";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000567;"bande de poignet";;"395 / 641 (époque byzantine)";"E 29033; Guimet 961";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000568;orbiculus;;"395 / 641 (époque byzantine)";"E 29036; Guimet 964";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000569;tabula;;"395 / 641 (époque byzantine)";"E 29037; Guimet 965";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000570;orbiculus;;"395 / 641 (époque byzantine)";"E 29038; Guimet 966";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000571;"décor de textile";;"395 / 641 (époque byzantine)";"E 29039; Guimet 967";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000572;clavus;;"395 / 641 (époque byzantine)";"E 29040; Guimet 968";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000573;orbiculus;;"395 / 641 (époque byzantine)";"E 29041; Guimet 969";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000574;tabula;;"395 / 641 (époque byzantine)";"E 29042; Guimet 970";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000575;tabula;;"395 / 641 (époque byzantine)";"E 29043; Guimet 971";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000576;tabula;;"395 / 641 (époque byzantine)";"E 29044; Guimet 972";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000577;clavus;;"395 / 641 (époque byzantine)";"E 29045; Guimet 973";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000578;clavus;;"395 / 641 (époque byzantine)";"E 29047; Guimet 975";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000579;tabula;;"395 / 641 (époque byzantine)";"E 29058; Guimet 986";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000580;orbiculus;;"395 / 641 (époque byzantine)";"E 29059; Guimet 987";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000581;clavus;;"395 / 641 (époque byzantine)";"E 29061; Guimet 989";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000582;tabula;;"395 / 641 (époque byzantine)";"E 29062; Guimet 990";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000583;"décor de textile";;"395 / 641 (époque byzantine)";"E 29063; Guimet 991";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000585;tabula;;"395 / 641 (époque byzantine)";"E 29075; Guimet 1003";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000609;clavus;;"395 / 641 (époque byzantine)";"E 29109; Guimet 1037";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000610;"bande décorative d'habillement";;"395 / 641 (époque byzantine)";"E 29110; Guimet 1038";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000630;"bande d'encolure";;"395 / 641 (époque byzantine)";"E 29145; Guimet 1073";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000637;"décor de textile";;"395 / 641 (époque byzantine)";"E 29152; Guimet 1080";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000662;écharpe;;"395 / 641 (époque byzantine)";"E 29191; Guimet 1119";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000664;"tissu ; fragments";;"395 / 641 (époque byzantine)";"E 29194; Guimet 1122";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000666;"clochette ; crotale";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"AF 1141";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000676;"lampe ovale allongée à canal";;"395 / 641 (époque byzantine)";"E 29989";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000677;"lampe ovale allongée à canal";;"395 / 641 (époque byzantine)";"AF 1263";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000678;"lampe ovale allongée à canal";;"395 / 641 (époque byzantine)";"AF 1515";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000679;"lampe ovoïde";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 22488; MG 4989";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000680;"lampe ovoïde";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"AF 1522";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000681;"lampe ovoïde";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"AF 1516";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000683;"lampe théière";;"395 / 641 (époque byzantine)";"AF 5120";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000685;lampe;;"395 / 641 (époque byzantine)";"AF 1261";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000687;"lampe ovale allongée à canal";;"395 / 641 (époque byzantine)";"AF 1508";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000688;"lampe ovale à médaillon et anse";;"395 / 641 (époque byzantine)";"AF 1525";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000689;"lampe ovale";;"395 / 641 (époque byzantine)";"E 15447";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000690;"panneau décoratif";;"1171 / 1510 (Ayyoubides ; Mamlouks)";"E 14285; X 5505";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000692;"flacon cylindrique";;"800 / 999 (début époque islamique)";"AF 1212";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000695;"flacon poisson";;"975 / 1125 (?) (début époque islamique [?])";"AF 1215";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000701;"vase ; aryballe";;"395 / 641 (?) (époque byzantine [?])";"AF 1472";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000703;vase;;"395 / 641 (époque byzantine)";"M. 3431";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000704;bassin;;"395 / 641 (?) (époque byzantine [?])";"E 12998";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000705;coupe;;"395 / 641 (époque byzantine)";"E 12999";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000706;"couvercle de vase";;"395 / 641 (époque byzantine)";"E 15420; M. 5693";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000707;"lampe ovoïde à bec pincé";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 21049; MG 3550";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000708;"lampe ovoïde à bec pincé";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 29958";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000709;"lampe ovoïde à bec pincé";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 21037; MG 3538";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000710;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"E 15442";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000711;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"E 15443";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000712;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"E 15444";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000713;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"E 15445";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000714;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"E 15446";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000716;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"E 15449";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000717;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"E 15450";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000718;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"E 29967";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000719;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"E 29969";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000720;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"AF 1492";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000721;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"AF 1494";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000722;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"AF 1514";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000723;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"AF 1526";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000724;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"AF 13019";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000728;figurine;;"395 / 641 (époque byzantine)";"E 13012 ?";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000737;figurine;;"-30 / 641 (?) (époque romaine [?] ; époque byzantine [?])";"E 14155";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000738;"pendentif ; amulette";;"395 / 641 (époque byzantine)";"AF 10993";"Département des Antiquités égyptiennes";;"Vif (France), Musée Champollion"; +cl010000741;figurine;;"-30 / 641 (?) (époque romaine [?] ; époque byzantine [?])";"E 13911; M. 4096";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000742;figurine;;"-30 / 395 (?) (époque romaine [?])";"E 14159; M. 4957";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000743;vase;;"-30 / 641 (?) (époque romaine [?] ; époque byzantine [?])";"E 15425; M. 4955";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000744;figurine;;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"AF 1479";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000748;figurine;;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"AF 8501";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000755;figurine;;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"AF 8503";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000756;figurine;;"395 / 641 (époque byzantine)";"E 14158";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000763;figurine;;"395 / 641 (époque byzantine)";"E 12781";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000767;figurine;;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"AF 1221";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000770;figurine;;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"AF 1480";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000780;figurine;;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 15429";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000781;figurine;;"-30 / 641 (?) (époque romaine [?] ; époque byzantine [?])";"E 13907";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000782;figurine;;"395 / 641 (époque byzantine)";"E 15430";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000784;figurine;;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"AF 8504";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000789;"lampe théière";;"395 / 641 (époque byzantine)";"E 13000";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000791;poinçon;;"-30 / 641 (?) (époque romaine [?] ; époque byzantine [?])";"AF 478";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000792;"plateau de jeu";;"650 / 700 (époque byzantine)";"E 21047; E 22316; MG 3548";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000793;"lampe à la grenouille";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 16117";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000794;lampe;;"395 / 641 (époque byzantine)";"E 12930";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000795;"lampe théière";;"500 / 599 (époque byzantine)";"AF 1521";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000796;"lampe théière";;"500 / 599 (époque byzantine)";"AF 1485";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000797;"lampe théière";;"500 / 599 (époque byzantine)";"AF 1486";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000798;"lampe théière";;"500 / 599 (époque byzantine)";"AF 1487";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000799;"lampe théière";;"500 / 599 (époque byzantine)";"AF 1488";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000800;"lampe ovale à médaillon et anse";;"395 / 641 (époque byzantine)";"E 29983";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000801;"lampe ovale à médaillon et anse";;"395 / 641 (époque byzantine)";"E 29984";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000802;"lampe ovale à médaillon et anse";;"395 / 641 (époque byzantine)";"E 21050; MG 3551";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000803;"lampe ovale à médaillon et anse";;"395 / 641 (époque byzantine)";"E 12392";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000804;"lampe ovale à médaillon et anse";;"300 / 499 (?) (époque byzantine ; époque romaine [?])";"E 15455";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000805;"lampe ovale à médaillon et anse";;"300 / 499 (?) (époque byzantine [?] ; époque romaine [?])";"E 12934";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000806;"lampe ovale allongée à canal";;"395 / 641 (époque byzantine)";"AF 1511";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000809;"lampe ovale à médaillon et anse";;"395 / 641 (époque byzantine)";"E 15454";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000810;"lampe ovale à médaillon et anse";;"395 / 641 (époque byzantine)";"AF 1506";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000811;"bracelet ; anneau";;"1171 / 1250 (Ayyoubides)";"E 10955 C";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000812;"bracelet ; anneau";;"1171 / 1250 (Ayyoubides)";"E 10955 D; AF 6854";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000819;"moule à lampe";;"700 / 799 (époque islamique)";"E 33070";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000820;moule;;"395 / 641 (époque byzantine)";"AF 13322";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000821;figurine;;"395 / 641 (époque byzantine)";"E 12779";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000822;figurine;;"-30 / 395 (?) (époque romaine [?])";"E 11748; N°65";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000823;"bracelet ; anneau";;"1171 / 1250 (Ayyoubides)";"E 10955 B";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000824;bracelet;;"1171 / 1250 (Ayyoubides)";"E 10955 A";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000828;"moule ; sceau";;"-332 / 395 (?) (époque ptolémaïque [?] ; époque romaine [?])";"E 12851";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000829;moule;;"395 / 641 (époque byzantine)";"E 12862";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000830;moule;;"395 / 641 (époque byzantine)";"E 12857";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000831;moule;;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"AF 13324";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000833;moule;;"395 / 641 (époque byzantine)";"E 12855";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000834;"moule à lampe";;"400 / 641 (époque byzantine)";"E 12856";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000839;vase;;"395 / 499 (époque byzantine)";"E 12957; M. 1848";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000840;moule;;"395 / 641 (?) (époque byzantine [?])";"AF 13321";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000842;moule;;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"AF 13320";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000843;"moule à lampe";;"395 / 641 (époque byzantine)";"E 33069";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000844;"moule à lampe";;"500 / 641 (époque byzantine)";"E 12846 ?";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000845;"moule à lampe";;"400 / 600 (époque byzantine)";"E 12847";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000846;"moule ; sceau";;"-332 / 395 (?) (époque ptolémaïque [?] ; époque romaine [?])";"E 12850";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000847;moule;;"500 / 699 (époque byzantine)";"E 12853";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000849;verrou;;"641 / 1952 (époque islamique)";"E 14055 A";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000850;"collier ; perle en olive ; perle sphérique ; pendentif ; amulette";;"-30 / 395 (époque romaine ; époque moderne)";"E 11911";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000852;"moule à lampe";;"395 / 641 (époque byzantine)";"AF 13325";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000853;moule;;"395 / 641 (époque byzantine)";"E 12860";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000855;moule;;"395 / 641 (?) (époque byzantine [?])";"AF 13323";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000856;"bracelet ; anneau ouvert";;"395 / 641 (époque byzantine)";"E 14244";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000858;"ampoule de St Ménas";;"395 / 641 (époque byzantine)";"E 21048; MG 3549";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000859;figurine;;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 12441";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000861;bonnet;;"969 / 1171 (Fatimides)";"AF 13231";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000865;"moule d'orfèvre";;"641 / 1952 (?) (époque islamique [?])";"E 11581";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000866;"moule d'orfèvre";;"-30 / 1952 (?) (époque islamique [?] ; époque romaine [?])";"E 11585";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000868;pot;;"-30 / 395 (époque romaine)";"E 30702";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000870;"marmite cannelée";;"-30 / 395 (époque romaine)";"E 30165";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000871;"marmite cannelée";;"-30 / 395 (époque romaine)";"E 30168";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000872;"moule d'orfèvre";;"-664 / 1952 (?) (époque islamique [?] ; époque romaine [?]; Basse Époque [?])";"AF 13505; Durand n°1899";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000873;"perle tubulaire";;"641 / 1952 (époque islamique)";"N 1945";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000885;corbeau;;"640 / 710 (Héraclius II ; Califat rashidun ; Omeyyades)";"AF 6312; X 5500";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000897;"figurine ; applique";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"AF 1484";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000898;"pendentif ; monnaie";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 21104; MG 3605";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000899;"médaillon ; monnaie";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 21105; MG 3606";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000900;"épingle à cheveux";;"395 / 641 (époque byzantine)";"E 21276; MG 3777";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000902;frise;;"750 / 905 (Abbasides ; Tulunides)";"AF 4821; AF 4911; AF 5171; X 3293; X 3385; X 3651";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000903;pendentif;;"395 / 641 (époque byzantine)";"E 13555";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000905;lampe;;"395 / 641 (époque byzantine)";"E 14149";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000906;"lampe ovoïde à bec pincé";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 29957";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000907;lampe;;"395 / 641 (époque byzantine)";"E 12889";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000908;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"E 13915";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000915;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 6634";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000916;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 6634";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000918;papyrus;;;"E 6649";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000930;flacon;;"395 / 641 (époque byzantine)";"AF 1102";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000931;"flacon ; verrerie";;"800 / 899 (Abbasides)";"E 12542";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000932;"flacon ; verrerie";;"800 / 899 (Abbasides)";"E 12543";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000953;"pendentif ; amulette";;"395 / 641 (époque byzantine)";"AF 966";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000968;moule;;"400 / 641 (époque byzantine)";"E 12858; AF 6944";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000971;vase;;"-2106 / -1786 (?) (Moyen Empire [?])";"E 12806";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000972;"flacon cylindrique";;"800 / 999 (début époque islamique)";"AF 1213";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000976;"peigne à tisser";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 12274; A 50";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000977;"lampe théière";;"300 / 395 (époque romaine)";"E 14147";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000979;"lampe théière";;"500 / 599 (époque byzantine)";"N 1057";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000980;"lampe théière";;"500 / 599 (époque byzantine)";"AF 1520";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000981;"lampe théière";;"500 / 599 (époque byzantine)";"AF 1519";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000982;"lampe théière";;"395 / 1952 (?) (époque byzantine [?] ; époque islamique [?])";"AF 1489";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000983;"lampe ovale à médaillon et anse";;"395 / 641 (époque byzantine)";"AF 1524";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000986;"lampe ovale à médaillon et anse";;"395 / 641 (époque byzantine)";"E 12933";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000987;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"E 12938";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000988;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"E 12943";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000989;vase;;"395 / 641 (époque byzantine)";"E 30120";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000990;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"AF 13020";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000991;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"AF 13021";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000993;pot;;"-30 / 395 (époque romaine)";"E 30162";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000995;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"E 32577";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000996;"lampe ovoïde";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 30807";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000997;lampe;;"395 / 641 (époque byzantine)";"E 30803";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000998;"lampe ovale à médaillon et anse";;"395 / 641 (époque byzantine)";"E 30802";"Département des Antiquités égyptiennes";;"non exposé"; +cl010000999;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"E 30799";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001000;"lampe à la grenouille";;"395 / 641 (époque byzantine)";"E 30798";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001001;"lampe ovoïde";;"395 / 641 (époque byzantine)";"E 14151";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001002;"lampe ovoïde";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 15583";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001003;"lampe ovoïde";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 29960";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001004;"lampe ovoïde";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 12213";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001005;"lampe ovoïde à bec pincé";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"AF 1518";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001007;vase;;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 30563";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001008;"coupe basse évasée";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 30226";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001009;cruche;;"395 / 641 (époque byzantine)";"E 30573";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001010;vase;;"395 / 641 (époque byzantine)";"E 30564";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001013;cruche;;"-30 / 395 (?) (époque romaine [?])";"E 30554";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001014;pot;;"395 / 641 (époque byzantine)";"E 30142";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001017;"bol caréné à bord droit et base annulaire";;"395 / 641 (époque byzantine)";"E 30375";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001018;vase;;"641 / 1952 (époque islamique)";"AF 9894";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001019;"moule à lampe";;"395 / 641 (époque byzantine)";"E 12812";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001026;"flacon ; verrerie";;"641 / 1952 (époque islamique)";"AF 958 2";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001028;flacon;;"641 / 1952 (époque islamique)";"AF 1211";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001041;flacon;;"800 / 899 (Abbasides)";"E 12541";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001043;"lampe ovoïde";;"400 / 599 (époque byzantine)";"E 15457";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001044;"lampe ovoïde";;"400 / 599 (époque byzantine)";"E 15459";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001045;lampe;;"395 / 641 (époque byzantine)";"AF 1244";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001046;"lampe ovale à médaillon et anse";;"300 / 499 (?) (époque byzantine [?] ; époque romaine [?])";"AF 1262";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001047;"lampe ovale à médaillon et anse";;"400 / 599 (époque byzantine)";"E 29985";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001048;"lampe ovale à médaillon et anse";;"300 / 499 (?) (époque byzantine [?] ; époque romaine [?])";"E 12390";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001049;"lampe ovale à médaillon et anse";;"395 / 641 (époque byzantine)";"AF 1523";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001050;"lampe de type syro palestinien";;"395 / 641 (époque byzantine)";"E 29943";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001051;"lampe de type syro palestinien";;"395 / 641 (époque byzantine)";"E 29944";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001052;"lampe de type syro palestinien";;"395 / 641 (époque byzantine)";"AF 1509";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001055;"peigne double";;"395 / 641 (époque byzantine)";"E 13505 1";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001056;"peigne double";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 13505 2";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001057;corbeau;;"395 / 641 (époque byzantine)";"AF 1361";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001058;"étiquette de momie en palette";;"100 / 299 (époque romaine)";"E 9557";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001059;"étiquette de momie rectangulaire";;"100 / 299 (époque romaine)";"E 9558";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001060;"étiquette de momie en palette";;"200 / 399 (époque romaine)";"E 9559";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001061;"étiquette de momie rectangulaire";;"100 / 299 (époque romaine)";"E 9560";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001062;"étiquette de momie en palette";;"100 / 299 (époque romaine)";"E 9561";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001063;"étiquette de momie en palette";;"100 / 299 (époque romaine)";"E 9562";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001064;"étiquette de momie en palette";;"100 / 299 (époque romaine)";"E 9563";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001065;"étiquette de momie en palette";;"100 / 299 (époque romaine)";"E 9564";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001066;"étiquette de momie en palette";;"100 / 299 (époque romaine)";"E 9565";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001070;polycandelon;;"400 / 900 (?) (époque byzantine [?] ; époque islamique [?])";"E 13524; X 3629";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001071;polycandelon;;"800 / 999 (début époque islamique)";"E 11711; X 3916";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001073;pot;;"395 / 641 (époque byzantine)";"E 30161";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001074;pot;;"-30 / 395 (époque romaine)";"E 30159";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001075;vase;;"395 / 641 (époque byzantine)";"E 30212";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001076;vase;;"395 / 641 (époque byzantine)";"E 30214";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001077;lampe;;"395 / 641 (époque byzantine)";"E 14148";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001078;lampe;;"395 / 641 (époque byzantine)";"AF 1152";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001079;"lampe ovoïde ; lampe à anse";;"395 / 641 (?) (époque byzantine [?])";"E 29987";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001080;"lampe ovoïde";;"395 / 641 (époque byzantine)";"E 12932";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001081;"lampe ovoïde à bec pincé";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 12397";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001082;lampe;;"395 / 641 (époque byzantine)";"E 21036; MG 3537";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001083;vase;;"395 / 641 (époque byzantine)";"E 30595";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001093;écharpe;;"430 / 610 (époque byzantine ; époque islamique)";"E 32155";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001096;vase;;"395 / 641 (époque byzantine)";"E 12875; X 3221";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001097;"sceau à poignée";;"395 / 641 (époque byzantine)";"AF 1466";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001098;"sceau à poignée";;"395 / 641 (époque byzantine)";"AF 1467";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001100;coupe;;"400 / 599 (époque byzantine)";"E 12814";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001101;coupe;;"395 / 641 (époque byzantine)";"E 12818; AF 5341";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001102;plat;;"395 / 641 (époque byzantine)";"E 12824";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001103;coupe;;"395 / 641 (époque byzantine)";"E 12816";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001104;coupe;;"400 / 599 (époque byzantine)";"E 12815";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001105;vase;;"395 / 641 (époque byzantine)";"E 12819";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001106;coupe;;"395 / 641 (époque byzantine)";"E 12821";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001107;assiette;;"395 / 641 (époque byzantine)";"E 12822";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001109;"sceau cylindre ; sceau médaillon";;"395 / 641 (époque byzantine)";"AF 1465";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001132;fuseau;;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 12293; A 68";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001134;calame;;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"AF 1177";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001136;"élément architectural ; décor architectural";;"395 / 641 (époque byzantine)";"AF 4878";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001137;"bouchon de vase";;"395 / 641 (époque byzantine)";"AF 1362";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001140;"papyrus littéraire ; Chant 18 de l'Iliade d'Homère";;"100 / 199 (Haut Empire)";"E 3060";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001155;"papyrus documentaire";;"-127 / -126 (Ptolémée VIII Evergète II)";"N 2330; Salt n°100 ?";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001156;"papyrus documentaire";;-98;"N 2331; Salt n°99";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001157;"papyrus documentaire";;-129;"N 2332; Salt n°98";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001158;"papyrus documentaire";;"-332 / -30 (époque ptolémaïque)";"N 2333";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001159;"papyrus documentaire";;-156;"N 2334";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001160;"papyrus documentaire";;-157;"N 2335; Drovetti n°2 (15)";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001162;"papyrus documentaire";;-127;"N 2337; Salt n°97";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001163;"papyrus documentaire";;-119;"N 2338; Salt n°102";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001164;"papyrus documentaire";;-241;"N 2338 BIS; Salt n°108";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001165;"papyrus documentaire";;"395 / 641 (?) (époque byzantine [?])";"E 6731";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001166;"papyrus documentaire";;"-1045 / -992 (Menkheperrê)";"E 25359";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001167;"papyrus documentaire";;"-1069 / -945 (XXIe dynastie)";"E 25363";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001168;"papyrus documentaire";;"-2500 / -2350 (Ve dynastie)";"E 25416 A 1";"Département des Antiquités égyptiennes";;Louvre-Lens; +cl010001169;"papyrus documentaire";;"-2500 / -2350 (Ve dynastie)";"E 25416 A 3";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001170;"papyrus documentaire";;"-2500 / -2350 (Ve dynastie)";"E 25280";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001171;"papyrus documentaire";;"-2500 / -2350 (Ve dynastie)";"E 25279";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001172;"papyrus littéraire";;"-1069 / -664 (Troisième Période intermédiaire)";"E 25352";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001173;"papyrus littéraire ; papyrus documentaire";;"-1069 / -332 (Troisième Période intermédiaire ; Basse Époque)";"E 25351";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001174;"papyrus documentaire";;"-2500 / -2350 (Ve dynastie)";"E 25416 C";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001175;"papyrus documentaire";;"-2411 / -2380 (Djedkarê Isési)";"E 25416 A 4";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001176;"papyrus documentaire";;"-2500 / -2350 (Ve dynastie)";"E 25416 A 2";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001177;fusaïole;;"395 / 641 (époque byzantine)";"AF 9421";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001184;fuseau;;"395 / 641 (époque byzantine)";"AF 1352";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001191;"peigne double ; tissu ; fil";;"395 / 641 (époque byzantine)";"E 21158 BIS; MG 3659 Bis";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001192;"peigne double";;"395 / 641 (époque byzantine)";"E 21159 BIS; MG 3660";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001193;"peigne double";;"395 / 641 (époque byzantine)";"E 21162 BIS; MG 3663";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001194;"relief mural";;"395 / 641 (époque byzantine)";"AF 10033";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001205;papyrus;;;"E 7131 A";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001206;papyrus;;;"E 7131 B";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001210;papyrus;;;"E 7131";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001237;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 7077";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001242;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 7053 R";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001247;"papyrus documentaire";;"600 / 699 (époque byzantine ; époque islamique)";"E 7007 B; E 7004 ERR";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001248;"frise ; décor architectural";;"395 / 641 (époque byzantine)";"AF 9271";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001250;"peigne double";;"700 / 799 (époque islamique)";"E 13504 1; X 5523";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001251;"peigne double";;"395 / 641 (époque byzantine)";"E 13504 2; X 5367";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001252;"peigne double";;"395 / 641 (époque byzantine)";"E 13504 3; X 5180";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001253;"peigne double";;"500 / 750 (époque byzantine ; Califat rashidun ; Omeyyades)";"E 13504 4; X 5366";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001254;"peigne double";;"395 / 641 (époque byzantine)";"E 13504 5";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001255;frise;;"500 / 699 (époque byzantine ; début époque islamique)";"AF 5169; X 3649";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001256;"instrument d'éclairage";;"395 / 641 (époque byzantine)";"E 11924; X 5222";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001257;lampe;;"395 / 641 (époque byzantine)";"E 11793";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001258;"papyrus documentaire";;"-199 / -100 (époque ptolémaïque)";"N 2339";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001259;"papyrus documentaire";;"200 / 299 (époque romaine)";"N 2340; Salt n°106";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001260;"papyrus documentaire";;"100 / 199 (époque romaine)";"N 2341; Salt n°203";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001261;"papyrus magique";;137;"N 2342; Salt n°104";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001262;"papyrus magique";;"138 / 161 (Antonin le Pieux)";"N 2342 BIS; Salt n°105";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001265;"papyrus documentaire";;-164;"N 2347";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001266;"papyrus documentaire";;-162;"N 2349; Drovetti n°2 (13)";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001267;"papyrus documentaire";;"-162 / -161 (Ptolémée VI Philométor)";"N 2350";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001268;"papyrus documentaire";;"-162 / -161 (époque ptolémaïque)";"N 2351; Drovetti n°1 (17)";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001269;"papyrus documentaire";;"-161 / -160 (Ptolémée VI Philométor)";"N 2352";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001270;"papyrus documentaire";;-161;"N 2353";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001271;"papyrus documentaire";;-162;"N 2354";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001272;"papyrus documentaire";;-161;"N 2355";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001274;"papyrus documentaire";;"-199 / -100 (époque ptolémaïque)";"N 2357; Salt n°102";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001275;"papyrus documentaire";;-163;"N 2358";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001276;"étiquette de momie en palette";;"100 / 299 (époque romaine)";"E 9595";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001277;"étiquette de momie en palette";;"200 / 299 (époque romaine)";"E 9596";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001279;"étiquette de momie en palette";;"100 / 299 (époque romaine)";"E 9614";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001280;"étiquette de momie à 1 anse";;"100 / 299 (époque romaine)";"E 9615";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001281;"étiquette de momie rectangulaire";;"200 / 299 (époque romaine)";"E 9811";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001283;"étui à kohol";;"395 / 641 (époque byzantine)";"E 10673";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001287;"panneau décoratif";;"395 / 641 (époque byzantine)";"AF 4872 a; X 3346 a";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001288;"panneau décoratif";;"395 / 641 (époque byzantine)";"AF 4872 b; X 3346 b";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001289;"décor architectural";;"395 / 641 (époque byzantine)";"AF 9270";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001291;"papyrus documentaire";;"582 / 602 (Maurice Ier Tiberius)";"N 2343";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001292;"papyrus documentaire";;"610 / 641 (Héraclius Ier)";"N 2344; N 2344 A; N 2344 B";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001293;"papyrus documentaire";;-163;"N 2345";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001294;"papyrus documentaire";;-163;"N 2346";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001311;"peigne double";;"395 / 641 (époque byzantine)";"E 32857; D.21.5.16; 71.1921.5.16 D";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001312;"amphore ; pot de saqqieh";;"-30 / 395 (époque romaine)";"E 32870; D.21.5.41; 71.1921.5.41 D";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001313;"peigne à tisser";;"395 / 641 (époque byzantine)";"E 32871; 71.1921.5.43D; D.21.5.43";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001314;"marmite ; jarre";;"-332 / 641 (?) (époque ptolémaïque [?] ; époque byzantine [?])";"E 30705";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001317;parchemin;;"395 / 641 (époque byzantine)";"E 10024; D.86.2.31; R 191";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001322;"papyrus documentaire";;"450 / 550 (époque byzantine)";"E 10285";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001323;tablette;;"395 / 641 (?) (époque byzantine [?])";"TCL 6";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001324;"papyrus funéraire";;"-30 / 395 (époque romaine)";"E 3040; N 3176 H";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001327;"papyrus funéraire";;"-30 / 395 (époque romaine)";"N 3156; Salt n°53";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001328;"papyrus funéraire";;"117 / 138 (?) (Hadrien [?])";"N 3289; Salt n°52";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001329;"papyrus funéraire";;"-30 / 395 (époque romaine)";"N 3290; Salt n°54";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001332;parchemin;;"395 / 641 (?) (époque byzantine [?])";"E 7153 A";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001338;"couvercle de boîte";;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 21366 B; MG 3867";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001339;"frise ; décor architectural";;"395 / 641 (époque byzantine)";"AF 4791; X 3263";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001340;frise;;"395 / 641 (époque byzantine)";"AF 9269";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001341;navette;;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"E 11726";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001344;jouet;;"-30 / 641 (?) (époque byzantine [?] ; époque romaine [?])";"AF 1186";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001346;"papyrus documentaire";;-156;"N 2365";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001347;"papyrus documentaire";;-153;"N 2366";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001348;"papyrus documentaire";;-152;"N 2367; Drovetti n°1 (21)";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001350;"papyrus documentaire";;"500 / 699 (époque byzantine)";"E 6600";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001352;"papyrus documentaire";;"395 / 641 (?) (époque byzantine [?])";"E 6608";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001354;"papyrus documentaire";;"395 / 641 (?) (époque byzantine [?])";"E 6614";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001355;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 6614";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001356;"papyrus documentaire";;"600 / 699 (époque byzantine ; époque islamique)";"E 6614";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001357;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 6616";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001358;papyrus;;;"E 6616";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001359;papyrus;;;"E 6616";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001360;"poutre ; décor architectural";;"395 / 641 (époque byzantine)";"E 12138; AF 4896; X 3370";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001361;"poutre ; décor architectural";;"395 / 641 (époque byzantine)";"E 12139; IFB-5-12 bis";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001363;"support de lampe";;"395 / 641 (époque byzantine)";"E 11916 7";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001364;candélabre;;"300 / 500 (?) (époque byzantine [?] ; époque romaine [?])";"E 11916 2";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001365;candélabre;;"400 / 699 (époque byzantine)";"E 11792 a; X 5271";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001383;"papyrus documentaire";;"-162 (?)";"N 2348";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001392;"papyrus documentaire";;"587 / 617 (époque byzantine)";"E 7710";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001393;"boucle d'oreille";;"641 / 750 (Califat rashidun ; Omeyyades)";"E 27178";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001394;pendentif;;"395 / 641 (?) (époque byzantine [?])";"E 13717";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001396;lampe;;"395 / 641 (?) (époque byzantine [?])";"E 12391";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001402;clochette;;"395 / 641 (époque byzantine)";"E 14209; X 5231";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001403;"bracelet ; anneau";;"284 / 400 (Bas Empire ; époque byzantine)";"E 23648";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001404;"anneau ; bracelet";;"641 / 1952 (époque islamique)";"E 23649";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001405;"empreinte de sceau";;"969 / 1171 (Fatimides)";"E 23652";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001406;coupe;;"300 / 399 (Théodosiens ; Thraces)";"E 23654";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001407;"empreinte de sceau";;"969 / 1171 (Fatimides)";"E 23657";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001408;"empreinte de sceau";;"641 / 1952 (époque islamique)";"E 23658";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001410;"étui à calame";;"395 / 1952 (?) (époque byzantine [?] ; époque islamique [?])";"E 24277";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001411;"anneau ; bracelet";;"641 / 1952 (époque islamique)";"E 23647";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001413;"boucle de ceinture";;"450 / 750 (Thraces ; Justiniens ; Héraclides ; Califat rashidun ; Omeyyades)";"AF 1136";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001414;"lampe delphiniforme";;"400 / 600 (époque byzantine)";"E 11916 12";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001415;"pendentif ; amulette";;"395 / 641 (époque byzantine)";"AF 10992";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001416;flacon;;"641 / 1952 (époque islamique)";"E 24095";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001417;flacon;;"641 / 1952 (époque islamique)";"E 24106";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001418;"lampe plastique";;"-30 / 641 (?) (époque romaine [?] ; époque byzantine [?])";"E 11865; X 5248";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001419;"lampe à anse";;"395 / 518 (Théodosiens ; Thraces)";"E 11684 BIS; X 5246";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001422;"lampe à 2 becs ; lampe à réflecteur";;"395 / 641 (époque byzantine)";"E 14211; X 5281";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001423;lampe;;"395 / 641 (?) (époque byzantine [?])";"E 11689; X 5256";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001424;"lampe delphiniforme";;"395 / 641 (époque byzantine)";"E 11916 1 b";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001428;figurine;;"395 / 641 (époque byzantine)";"E 24684";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001429;monnaie;;"395 / 641 (époque byzantine)";"E 32245";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001430;monnaie;;"395 / 641 (époque byzantine)";"E 32248";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001431;monnaie;;"395 / 641 (époque byzantine)";"E 32249";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001432;coupe;;"395 / 641 (époque byzantine)";"E 32515; IIT37d1";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001433;vase;;"395 / 641 (époque byzantine)";"E 32451; IT15s4";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001434;vase;;"395 / 641 (époque byzantine)";"E 32452; IT18s2";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001435;pelote;;"527 / 600 (époque islamique)";"E 12622";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001437;coupe;;"395 / 641 (époque byzantine)";"E 32516; IIT37d2";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001438;tunique;;"395 / 641 (époque byzantine)";"E 29219; Guimet 1147";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001439;pelote;;"540 / 624 (époque islamique)";"E 12621";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001441;"frise ; bloc de paroi ; relief mural";;"550 / 750 (époque byzantine ; début époque islamique)";"E 14748; X 4074";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001442;"bloc de frise";;"550 / 750 (époque byzantine ; début époque islamique)";"E 14751";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001443;plat;;"641 / 1952 (époque islamique)";"E 32835; D.21.5.38";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001444;plat;;"641 / 1952 (époque islamique)";"E 32836; D.21.5.40";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001448;"stèle funéraire";;"395 / 641 (époque byzantine)";"C 233; N 323; Salt n°3726 ?; MR 5701; LP 1813 ?";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001454;"papyrus documentaire";;"117 / 138 (?) (Hadrien [?])";"N 2376 BIS; Salt n°107";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001455;"papyrus documentaire";;-158;"N 2378; N 2392";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001456;"papyrus documentaire";;-153;"N 2382";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001457;"papyrus documentaire";;-159;"N 2383";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001458;"papyrus documentaire";;-154;"N 2384";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001459;"papyrus documentaire";;-156;"N 2386; Drovetti n°1 (27)";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001464;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 6869";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001476;"papyrus documentaire";;"395 / 641 (?) (époque byzantine [?])";"E 7038";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001497;papyrus;;"395 / 641 (époque byzantine)";"E 6910";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001498;"papyrus documentaire";;"395 / 641 (?) (époque byzantine [?])";"E 6910";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001499;"papyrus documentaire";;"500 / 699 (époque byzantine ; époque islamique)";"E 6910";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001500;"papyrus documentaire";;"500 / 599 (époque byzantine)";"E 6910";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001501;papyrus;;"395 / 641 (époque byzantine)";"E 6910";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001502;papyrus;;"395 / 641 (époque byzantine)";"E 6910";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001506;"papyrus documentaire";;"395 / 641 (?) (époque byzantine [?])";"E 6920";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001514;"papyrus documentaire";;"-199 / -100 (époque ptolémaïque)";"N 2389; Salt n°108";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001515;"papyrus documentaire";;"-180 / -145 (?) (Ptolémée VI Philométor)";"N 2390; Salt n°101";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001516;"papyrus documentaire";;-145;"N 2390 Bis; Salt n°291";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001517;"papyrus magique";;"300 / 399 (époque romaine)";"N 2391; LP 1690; Mimaut n°541";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001518;"papyrus documentaire";;"-332 / -30 (époque ptolémaïque)";"N 2325; N 2388";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001520;"papyrus documentaire";;-152;"N 2369; Drovetti n°1 (23)";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001521;"papyrus documentaire";;-152;"N 2370; Drovetti n°1 (24)";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001522;"papyrus documentaire";;-152;"N 2371; Drovetti n°1 (25)";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001523;"papyrus documentaire";;-160;"N 2372";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001524;"papyrus documentaire";;-159;"N 2374";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001525;papyrus;;;"E 7131 C";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001541;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 7120";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001549;"papyrus documentaire";;"500 / 699 (époque byzantine ; époque islamique)";"E 7121";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001555;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 7331";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001556;"papyrus littéraire ; Chant 13 de l'Iliade d'Homère";;"-99 / 99 (époque ptolémaïque ; époque romaine)";"N 2327; Salt n°109 ?; Drovetti n°2 ?";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001561;"papyrus documentaire";;"-30 / 395 (?) (époque romaine [?])";"N 2393";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001562;papyrus;;;"N 2394";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001563;"papyrus documentaire";;"-30 / 395 (?) (époque romaine [?])";"N 2397";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001564;"papyrus documentaire";;"222 / 235 (Sévère Alexandre)";"N 2411 BIS";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001565;papyrus;;;"E 6738 B";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001576;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 7087";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001596;"papyrus littéraire ; papyrus documentaire";;"-199 / -150 (époque ptolémaïque)";"N 2326; N 2373";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001599;"papyrus documentaire";;"-161 / -157 (Ptolémée VI Philométor)";"N 2360";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001606;"papyrus littéraire ; Chant 6 de l'Iliade d'Homère";;"100 / 199 (Haut Empire)";"E 3338";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001607;"papyrus magique";;"0 / 99 (Haut Empire)";"N 3378";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001631;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 6588";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001633;papyrus;;533;"E 6863 F; E 6846";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001650;"papyrus documentaire";;"500 / 599 (époque byzantine)";"E 6474";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001658;"papyrus documentaire";;"500 / 699 (époque byzantine ; époque islamique)";"E 6485";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001659;"papyrus documentaire";;;"E 6485";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001665;"papyrus documentaire";;"395 / 641 (?) (époque byzantine [?])";"E 6498";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001687;"papyrus documentaire";;"395 / 641 (?) (époque byzantine [?])";"E 6562 A";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001688;"papyrus documentaire";;"395 / 641 (?) (époque byzantine [?])";"E 6562 B";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001696;parchemin;;"500 / 599 (époque byzantine)";"E 6609 D";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001697;parchemin;;"395 / 641 (époque byzantine)";"E 6609 E";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001722;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 10234";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001733;"papyrus documentaire";;"395 / 641 (?) (époque byzantine [?])";"E 6577";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001735;papyrus;;"395 / 641 (époque byzantine)";"E 6588";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001736;papyrus;;"500 / 699 (époque byzantine)";"E 6588";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001737;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 6588";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001738;parchemin;;"395 / 641 (époque byzantine)";"E 6622 B";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001739;parchemin;;"395 / 641 (?) (époque byzantine [?])";"E 6622 C";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001740;"papyrus documentaire";;"-2500 / -2350 (Ve dynastie)";"E 25416 A 5";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001741;"étiquette de jarre ; Etiquette de jarre de Den";;"-3100 / -2900 (Den)";"E 25268";"Département des Antiquités égyptiennes";;"Sully, [AE] Salle 634 - L'époque thinite, Galerie d'étude, Vitrine ge 2 "; +cl010001742;"papyrus documentaire ; Papyrus d'Abousir";;"-2500 / -2350 (Ve dynastie)";"E 25416 B";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001743;parchemin;;"395 / 641 (époque byzantine)";"E 6622 D";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001744;parchemin;;"600 / 699 (époque byzantine ; époque islamique)";"E 6609 A";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001745;parchemin;;"395 / 641 (?) (époque byzantine [?])";"E 6609 B";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001746;parchemin;;"395 / 641 (?) (époque byzantine [?])";"E 6609 C";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001747;"papyrus documentaire";;"395 / 641 (?) (époque byzantine [?])";"E 6589 A";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001748;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 6588";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001749;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 6594";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001750;papyrus;;"395 / 641 (?) (époque byzantine [?])";"E 6594";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001764;"étiquette de momie rectangulaire";;"100 / 299 (époque romaine)";"E 9956";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001765;"étiquette de momie en palette";;"100 / 299 (époque romaine)";"E 9623";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001766;"étiquette de momie rectangulaire";;"100 / 299 (époque romaine)";"E 9624";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001767;"étiquette de momie en palette";;"200 / 399 (époque romaine)";"E 9625";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001768;"étiquette de momie en palette";;"100 / 299 (époque romaine)";"E 9626";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001769;"étiquette de momie en palette";;205;"E 9627";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001770;"étiquette de momie rectangulaire";;"200 / 299 (époque romaine)";"E 9633";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001771;"étiquette de momie en palette";;"200 / 299 (époque romaine)";"E 9634";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001772;"étiquette de momie en palette";;"100 / 299 (époque romaine)";"E 9635";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001773;"étiquette de momie en palette";;"100 / 299 (époque romaine)";"E 9636";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001774;"étiquette de momie en palette";;"100 / 299 (époque romaine)";"E 9638";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001775;"étiquette de momie à 1 anse";;"100 / 299 (époque romaine)";"E 9639";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001776;"étiquette de momie en palette";;"100 / 299 (époque romaine)";"E 9640";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001777;"étiquette de momie rectangulaire";;"100 / 299 (époque romaine)";"E 9641";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001778;"étiquette de momie à 1 anse";;"200 / 299 (époque romaine)";"E 9642";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001779;"étiquette de momie rectangulaire";;"200 / 299 (époque romaine)";"E 9643";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001780;"étiquette de momie en palette";;"100 / 299 (époque romaine)";"E 9587";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001781;"étiquette de momie en palette";;"100 / 299 (époque romaine)";"E 9588";"Département des Antiquités égyptiennes";;"non exposé"; +cl010001782;"étiquette de momie rectangulaire";;"100 / 299 (époque romaine)";"E 9589";"Département des Antiquités égyptiennes";;"non exposé"; diff --git a/app/CDN/.air.toml b/app/cdn/.air.toml similarity index 100% rename from app/CDN/.air.toml rename to app/cdn/.air.toml diff --git a/app/CDN/config/config.go b/app/cdn/config/config.go similarity index 100% rename from app/CDN/config/config.go rename to app/cdn/config/config.go diff --git a/app/CDN/docs/structure.md b/app/cdn/docs/structure.md similarity index 100% rename from app/CDN/docs/structure.md rename to app/cdn/docs/structure.md diff --git a/app/CDN/go.mod b/app/cdn/go.mod similarity index 100% rename from app/CDN/go.mod rename to app/cdn/go.mod diff --git a/app/CDN/go.sum b/app/cdn/go.sum similarity index 100% rename from app/CDN/go.sum rename to app/cdn/go.sum diff --git a/app/cdn/internal/cache/cache.go b/app/cdn/internal/cache/cache.go new file mode 100644 index 0000000..01c14c2 --- /dev/null +++ b/app/cdn/internal/cache/cache.go @@ -0,0 +1,223 @@ +// Package cache fournit des implémentations de cache pour le CDN +// Il propose deux types de cache : en mémoire (LRU) et Redis +package cache + +import ( + "context" + "encoding/json" + "sync/atomic" + "time" + + lru "github.com/hashicorp/golang-lru" + "github.com/redis/go-redis/v9" +) + +// CacheMetrics contient les métriques de performance du cache +type CacheMetrics struct { + Hits uint64 + Misses uint64 + Items uint64 +} + + + + +// CacheEntry représente une entrée dans le cache avec TTL +type CacheEntry struct { + Value interface{} + Expiration time.Time + Headers map[string]string +} + +// Cache définit l'interface commune pour toutes les implémentations de cache +type Cache interface { + // Get récupère une valeur du cache à partir de sa clé + Get(ctx context.Context, key string) (*CacheEntry, bool, error) + + // Set stocke une valeur dans le cache avec la clé spécifiée + Set(ctx context.Context, key string, value interface{}, headers map[string]string, ttl time.Duration) error + + // Delete supprime une valeur du cache + Delete(ctx context.Context, key string) error + + // GetMetrics retourne les métriques du cache + GetMetrics() *CacheMetrics + + // Clear vide complètement le cache + Clear() +} + +// MemoryCache implémente un cache en mémoire utilisant l'algorithme LRU +type MemoryCache struct { + lru *lru.Cache + metrics CacheMetrics + maxSize int +} + +// NewMemoryCache crée une nouvelle instance de MemoryCache +func NewMemoryCache(size int) (*MemoryCache, error) { + l, err := lru.New(size) + if err != nil { + return nil, err + } + return &MemoryCache{ + lru: l, + maxSize: size, + }, nil +} + +// Get récupère une valeur du cache mémoire +func (m *MemoryCache) Get(ctx context.Context, key string) (*CacheEntry, bool, error) { + value, exists := m.lru.Get(key) + if !exists { + atomic.AddUint64(&m.metrics.Misses, 1) + return nil, false, nil + } + + entry := value.(*CacheEntry) + if time.Now().After(entry.Expiration) { + m.lru.Remove(key) + atomic.AddUint64(&m.metrics.Misses, 1) + return nil, false, nil + } + + atomic.AddUint64(&m.metrics.Hits, 1) + return entry, true, nil +} + +// Set ajoute ou met à jour une valeur dans le cache mémoire +func (m *MemoryCache) Set(ctx context.Context, key string, value interface{}, headers map[string]string, ttl time.Duration) error { + entry := &CacheEntry{ + Value: value, + Headers: headers, + Expiration: time.Now().Add(ttl), + } + + // Si la clé existe déjà, ne pas incrémenter le compteur + if _, exists := m.lru.Get(key); !exists { + // Si le cache est plein, le LRU va automatiquement évincer un élément + if m.lru.Len() >= m.maxSize { + atomic.AddUint64(&m.metrics.Items, ^uint64(0)) // Décrémente le compteur pour l'élément qui sera évincé + } + atomic.AddUint64(&m.metrics.Items, 1) + } + + m.lru.Add(key, entry) + return nil +} + +// Delete supprime une valeur du cache mémoire +func (m *MemoryCache) Delete(ctx context.Context, key string) error { + if m.lru.Remove(key) { + atomic.AddUint64(&m.metrics.Items, ^uint64(0)) + } + return nil +} + +// GetMetrics retourne les métriques du cache mémoire +func (m *MemoryCache) GetMetrics() *CacheMetrics { + return &CacheMetrics{ + Hits: atomic.LoadUint64(&m.metrics.Hits), + Misses: atomic.LoadUint64(&m.metrics.Misses), + Items: atomic.LoadUint64(&m.metrics.Items), + } +} + +// Clear vide complètement le cache mémoire +func (m *MemoryCache) Clear() { + m.lru.Purge() + atomic.StoreUint64(&m.metrics.Items, 0) +} + +// RedisCache implémente un cache distribué utilisant Redis +type RedisCache struct { + client *redis.Client + metrics CacheMetrics +} + +// crée une nouvelle instance de RedisCache +func NewRedisCache(url string, db int) (*RedisCache, error) { + client := redis.NewClient(&redis.Options{ + Addr: url, + DB: db, + }) + + // Test de connexion + ctx := context.Background() + if err := client.Ping(ctx).Err(); err != nil { + return nil, err + } + + return &RedisCache{client: client}, nil +} + +// Get récupère une valeur du cache Redis +func (r *RedisCache) Get(ctx context.Context, key string) (*CacheEntry, bool, error) { + data, err := r.client.Get(ctx, key).Bytes() + if err == redis.Nil { + atomic.AddUint64(&r.metrics.Misses, 1) + return nil, false, nil + } + if err != nil { + return nil, false, err + } + + var entry CacheEntry + if err := json.Unmarshal(data, &entry); err != nil { + return nil, false, err + } + + if time.Now().After(entry.Expiration) { + r.Delete(ctx, key) + atomic.AddUint64(&r.metrics.Misses, 1) + return nil, false, nil + } + + atomic.AddUint64(&r.metrics.Hits, 1) + return &entry, true, nil +} + +// Set stocke une valeur dans Redis +func (r *RedisCache) Set(ctx context.Context, key string, value interface{}, headers map[string]string, ttl time.Duration) error { + entry := &CacheEntry{ + Value: value, + Headers: headers, + Expiration: time.Now().Add(ttl), + } + + data, err := json.Marshal(entry) + if err != nil { + return err + } + + if err := r.client.Set(ctx, key, data, ttl).Err(); err != nil { + return err + } + + atomic.AddUint64(&r.metrics.Items, 1) + return nil +} + +// Delete supprime une valeur du cache Redis +func (r *RedisCache) Delete(ctx context.Context, key string) error { + if err := r.client.Del(ctx, key).Err(); err != nil { + return err + } + atomic.AddUint64(&r.metrics.Items, ^uint64(0)) + return nil +} + +// GetMetrics retourne les métriques du cache Redis +func (r *RedisCache) GetMetrics() *CacheMetrics { + return &CacheMetrics{ + Hits: atomic.LoadUint64(&r.metrics.Hits), + Misses: atomic.LoadUint64(&r.metrics.Misses), + Items: atomic.LoadUint64(&r.metrics.Items), + } +} + +// Clear vide complètement le cache Redis +func (r *RedisCache) Clear() { + r.client.FlushDB(context.Background()) + atomic.StoreUint64(&r.metrics.Items, 0) +} diff --git a/app/cdn/internal/cache/cache_test.go b/app/cdn/internal/cache/cache_test.go new file mode 100644 index 0000000..5f42365 --- /dev/null +++ b/app/cdn/internal/cache/cache_test.go @@ -0,0 +1,388 @@ +package cache + +import ( + "bytes" + "context" + "fmt" + "net/http" + "testing" + "time" +) + +func TestMemoryCache(t *testing.T) { + t.Run("Test création du cache", func(t *testing.T) { + cache, err := NewMemoryCache(100) + if err != nil { + t.Fatalf("Erreur lors de la création du cache: %v", err) + } + if cache == nil { + t.Fatal("Le cache ne devrait pas être nil") + } + }) + + t.Run("Test Set et Get basique", func(t *testing.T) { + cache, _ := NewMemoryCache(100) + ctx := context.Background() + key := "test-key" + value := "test-value" + headers := map[string]string{"Content-Type": "text/plain"} + + err := cache.Set(ctx, key, value, headers, time.Minute) + if err != nil { + t.Fatalf("Erreur lors du Set: %v", err) + } + + entry, exists, err := cache.Get(ctx, key) + if err != nil { + t.Fatalf("Erreur lors du Get: %v", err) + } + if !exists { + t.Fatal("La valeur devrait exister dans le cache") + } + if entry.Value != value { + t.Errorf("Valeur attendue %v, obtenue %v", value, entry.Value) + } + if entry.Headers["Content-Type"] != "text/plain" { + t.Errorf("Header Content-Type attendu 'text/plain', obtenu '%v'", entry.Headers["Content-Type"]) + } + }) + + t.Run("Test expiration", func(t *testing.T) { + cache, _ := NewMemoryCache(100) + ctx := context.Background() + key := "test-expiration" + value := "test-value" + + err := cache.Set(ctx, key, value, nil, time.Millisecond) + if err != nil { + t.Fatalf("Erreur lors du Set: %v", err) + } + + time.Sleep(time.Millisecond * 2) + + _, exists, err := cache.Get(ctx, key) + if err != nil { + t.Fatalf("Erreur lors du Get: %v", err) + } + if exists { + t.Error("La valeur devrait être expirée") + } + }) + + t.Run("Test Delete", func(t *testing.T) { + cache, _ := NewMemoryCache(100) + ctx := context.Background() + key := "test-delete" + value := "test-value" + + cache.Set(ctx, key, value, nil, time.Minute) + err := cache.Delete(ctx, key) + if err != nil { + t.Fatalf("Erreur lors du Delete: %v", err) + } + + _, exists, err := cache.Get(ctx, key) + if err != nil { + t.Fatalf("Erreur lors du Get après Delete: %v", err) + } + if exists { + t.Error("La valeur devrait être supprimée") + } + }) + + t.Run("Test métriques", func(t *testing.T) { + cache, _ := NewMemoryCache(100) + ctx := context.Background() + key := "test-metrics" + value := "test-value" + + // Test des misses + cache.Get(ctx, key) + metrics := cache.GetMetrics() + if metrics.Misses != 1 { + t.Errorf("Attendu 1 miss, obtenu %d", metrics.Misses) + } + + // Test des hits + cache.Set(ctx, key, value, nil, time.Minute) + cache.Get(ctx, key) + metrics = cache.GetMetrics() + if metrics.Hits != 1 { + t.Errorf("Attendu 1 hit, obtenu %d", metrics.Hits) + } + + // Test du nombre d'items + if metrics.Items != 1 { + t.Errorf("Attendu 1 item, obtenu %d", metrics.Items) + } + }) + + t.Run("Test limite de taille", func(t *testing.T) { + size := 2 + cache, _ := NewMemoryCache(size) + ctx := context.Background() + + // Ajouter plus d'éléments que la taille maximale + for i := 0; i < size+1; i++ { + key := fmt.Sprintf("key-%d", i) + cache.Set(ctx, key, i, nil, time.Minute) + } + + // Vérifier que le cache respecte sa taille maximale + metrics := cache.GetMetrics() + if metrics.Items > uint64(size) { + t.Errorf("Le cache contient %d items, devrait en contenir maximum %d", metrics.Items, size) + } + }) +} + +func TestRedisCache(t *testing.T) { + // Skip si Redis n'est pas disponible en local + redisURL := "redis://localhost:6379/0" + cache, err := NewRedisCache(redisURL, 0) + if err != nil { + t.Skip("Redis n'est pas disponible, tests ignorés") + } + + t.Run("Test Set et Get basique", func(t *testing.T) { + ctx := context.Background() + key := "test-redis-key" + value := "test-redis-value" + headers := map[string]string{"Content-Type": "text/plain"} + + err := cache.Set(ctx, key, value, headers, time.Minute) + if err != nil { + t.Fatalf("Erreur lors du Set Redis: %v", err) + } + + entry, exists, err := cache.Get(ctx, key) + if err != nil { + t.Fatalf("Erreur lors du Get Redis: %v", err) + } + if !exists { + t.Fatal("La valeur devrait exister dans Redis") + } + if entry.Value != value { + t.Errorf("Valeur Redis attendue %v, obtenue %v", value, entry.Value) + } + if entry.Headers["Content-Type"] != "text/plain" { + t.Errorf("Header Redis Content-Type attendu 'text/plain', obtenu '%v'", entry.Headers["Content-Type"]) + } + + // Nettoyage + cache.Delete(ctx, key) + }) + + // Les autres tests Redis suivent le même modèle que MemoryCache... +} + +type MockHTTPResponse struct { + Body []byte + StatusCode int + Headers map[string]string + RequestTime time.Time +} + +func TestCacheHTTPResponses(t *testing.T) { + cache, err := NewMemoryCache(100) + if err != nil { + t.Fatalf("Erreur création cache: %v", err) + } + ctx := context.Background() + + t.Run("Test mise en cache réponse HTTP", func(t *testing.T) { + // Simuler une réponse HTTP + originalResponse := MockHTTPResponse{ + Body: []byte("Contenu de test"), + StatusCode: http.StatusOK, + Headers: map[string]string{ + "Content-Type": "text/plain", + "Cache-Control": "max-age=3600", + }, + RequestTime: time.Now(), + } + + // Mettre en cache + err := cache.Set(ctx, "/test-url", originalResponse, originalResponse.Headers, time.Hour) + if err != nil { + t.Fatalf("Erreur mise en cache: %v", err) + } + + // Récupérer du cache + entry, exists, err := cache.Get(ctx, "/test-url") + if err != nil { + t.Fatalf("Erreur récupération cache: %v", err) + } + if !exists { + t.Fatal("La réponse devrait être en cache") + } + + // Vérifier le contenu + cachedResponse := entry.Value.(MockHTTPResponse) + if !bytes.Equal(cachedResponse.Body, originalResponse.Body) { + t.Error("Le contenu en cache ne correspond pas à l'original") + } + if cachedResponse.StatusCode != originalResponse.StatusCode { + t.Error("Le status code en cache ne correspond pas à l'original") + } + if entry.Headers["Content-Type"] != originalResponse.Headers["Content-Type"] { + t.Error("Les headers en cache ne correspondent pas à l'original") + } + }) + + t.Run("Test expiration réponse HTTP", func(t *testing.T) { + shortTTL := 100 * time.Millisecond + response := MockHTTPResponse{ + Body: []byte("Contenu qui expire vite"), + StatusCode: http.StatusOK, + Headers: map[string]string{ + "Content-Type": "text/plain", + }, + } + + // Mettre en cache avec TTL court + err := cache.Set(ctx, "/expire-test", response, response.Headers, shortTTL) + if err != nil { + t.Fatalf("Erreur mise en cache: %v", err) + } + + // Vérifier immédiatement + _, exists, _ := cache.Get(ctx, "/expire-test") + if !exists { + t.Fatal("La réponse devrait être en cache immédiatement") + } + + // Attendre l'expiration + time.Sleep(shortTTL + 50*time.Millisecond) + + // Vérifier après expiration + _, exists, _ = cache.Get(ctx, "/expire-test") + if exists { + t.Error("La réponse devrait être expirée et non disponible") + } + }) + + t.Run("Test mise à jour réponse HTTP", func(t *testing.T) { + key := "/update-test" + original := MockHTTPResponse{ + Body: []byte("Contenu original"), + StatusCode: http.StatusOK, + Headers: map[string]string{ + "Content-Type": "text/plain", + }, + } + + // Première mise en cache + err := cache.Set(ctx, key, original, original.Headers, time.Hour) + if err != nil { + t.Fatalf("Erreur première mise en cache: %v", err) + } + + // Mise à jour avec nouveau contenu + updated := MockHTTPResponse{ + Body: []byte("Contenu mis à jour"), + StatusCode: http.StatusOK, + Headers: map[string]string{ + "Content-Type": "text/plain", + "Updated": "true", + }, + } + + err = cache.Set(ctx, key, updated, updated.Headers, time.Hour) + if err != nil { + t.Fatalf("Erreur mise à jour cache: %v", err) + } + + // Vérifier le contenu mis à jour + entry, exists, _ := cache.Get(ctx, key) + if !exists { + t.Fatal("La réponse mise à jour devrait être en cache") + } + + cachedResponse := entry.Value.(MockHTTPResponse) + if !bytes.Equal(cachedResponse.Body, updated.Body) { + t.Error("Le contenu en cache ne correspond pas à la mise à jour") + } + if entry.Headers["Updated"] != "true" { + t.Error("Les headers mis à jour ne sont pas présents") + } + }) + + t.Run("Test suppression réponse HTTP", func(t *testing.T) { + key := "/delete-test" + response := MockHTTPResponse{ + Body: []byte("Contenu à supprimer"), + StatusCode: http.StatusOK, + Headers: map[string]string{ + "Content-Type": "text/plain", + }, + } + + // Mettre en cache + err := cache.Set(ctx, key, response, response.Headers, time.Hour) + if err != nil { + t.Fatalf("Erreur mise en cache: %v", err) + } + + // Supprimer + err = cache.Delete(ctx, key) + if err != nil { + t.Fatalf("Erreur suppression cache: %v", err) + } + + // Vérifier la suppression + _, exists, _ := cache.Get(ctx, key) + if exists { + t.Error("La réponse devrait être supprimée du cache") + } + }) +} + +func BenchmarkHTTPCache(b *testing.B) { + cache, _ := NewMemoryCache(1000) + ctx := context.Background() + response := MockHTTPResponse{ + Body: []byte("Contenu de benchmark"), + StatusCode: http.StatusOK, + Headers: map[string]string{ + "Content-Type": "text/plain", + }, + } + + b.Run("Mise en cache", func(b *testing.B) { + for i := 0; i < b.N; i++ { + key := fmt.Sprintf("/bench-key-%d", i) + cache.Set(ctx, key, response, response.Headers, time.Hour) + } + }) + + b.Run("Lecture cache", func(b *testing.B) { + key := "/bench-read" + cache.Set(ctx, key, response, response.Headers, time.Hour) + b.ResetTimer() + for i := 0; i < b.N; i++ { + cache.Get(ctx, key) + } + }) +} + +func BenchmarkCache(b *testing.B) { + cache, _ := NewMemoryCache(1000) + ctx := context.Background() + key := "bench-key" + value := "bench-value" + + b.Run("Set", func(b *testing.B) { + for i := 0; i < b.N; i++ { + cache.Set(ctx, fmt.Sprintf("%s-%d", key, i), value, nil, time.Minute) + } + }) + + b.Run("Get", func(b *testing.B) { + cache.Set(ctx, key, value, nil, time.Minute) + b.ResetTimer() + for i := 0; i < b.N; i++ { + cache.Get(ctx, key) + } + }) +} diff --git a/app/cdn/internal/cache/cdn_cache_test.go b/app/cdn/internal/cache/cdn_cache_test.go new file mode 100644 index 0000000..da3c42b --- /dev/null +++ b/app/cdn/internal/cache/cdn_cache_test.go @@ -0,0 +1,244 @@ +package cache + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" +) + +// simulerServeurOrigine crée un serveur de test qui simule un serveur d'origine +func simulerServeurOrigine(t *testing.T) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.Header().Set("X-Server", "Origine") + w.WriteHeader(http.StatusOK) + w.Write([]byte("Contenu du serveur d'origine")) + })) +} + +func TestCDNCache(t *testing.T) { + cache, err := NewMemoryCache(100) + if err != nil { + t.Fatalf("Erreur création cache: %v", err) + } + ctx := context.Background() + + // Créer un serveur d'origine simulé + serveurOrigine := simulerServeurOrigine(t) + defer serveurOrigine.Close() + + t.Run("Test mise en cache d'une requête CDN", func(t *testing.T) { + // Simuler une requête au serveur d'origine + resp, err := http.Get(serveurOrigine.URL + "/test-content") + if err != nil { + t.Fatalf("Erreur requête serveur origine: %v", err) + } + defer resp.Body.Close() + + // Lire le contenu de la réponse + contenu, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("Erreur lecture réponse: %v", err) + } + + // Créer une entrée de cache + headers := make(map[string]string) + for k, v := range resp.Header { + headers[k] = v[0] + } + + // Mettre en cache la réponse + err = cache.Set(ctx, "/test-content", contenu, headers, time.Hour) + if err != nil { + t.Fatalf("Erreur mise en cache: %v", err) + } + + // Vérifier que la réponse est en cache + entry, exists, err := cache.Get(ctx, "/test-content") + if err != nil { + t.Fatalf("Erreur récupération cache: %v", err) + } + if !exists { + t.Fatal("La réponse devrait être en cache") + } + + // Vérifier le contenu et les headers + cachedContent := entry.Value.([]byte) + if string(cachedContent) != string(contenu) { + t.Error("Le contenu en cache ne correspond pas à la réponse originale") + } + if entry.Headers["X-Server"] != "Origine" { + t.Error("Les headers en cache ne correspondent pas") + } + }) + + t.Run("Test performance du cache", func(t *testing.T) { + // Préparer les données de test + donnees := make([]struct { + url string + contenu []byte + }, 100) // Réduire à 100 pour un test plus réaliste + + for i := 0; i < 100; i++ { + donnees[i] = struct { + url string + contenu []byte + }{ + url: fmt.Sprintf("/perf-test-%d", i), + contenu: []byte(fmt.Sprintf("Contenu de test pour l'entrée %d", i)), + } + } + + // Test de mise en cache en série + debut := time.Now() + for _, d := range donnees { + err := cache.Set(ctx, d.url, d.contenu, nil, time.Hour) + if err != nil { + t.Fatalf("Erreur mise en cache: %v", err) + } + } + dureeSet := time.Since(debut) + t.Logf("Temps total de mise en cache (série): %v", dureeSet) + t.Logf("Temps moyen de mise en cache: %v/opération", dureeSet/100) + + // Test de lecture en série + var hits, misses int + debut = time.Now() + for _, d := range donnees { + entry, exists, err := cache.Get(ctx, d.url) + if err != nil { + t.Fatalf("Erreur lecture cache: %v", err) + } + if exists { + hits++ + // Vérifier l'intégrité des données + if string(entry.Value.([]byte)) != string(d.contenu) { + t.Errorf("Corruption des données pour %s", d.url) + } + } else { + misses++ + } + } + dureeGet := time.Since(debut) + t.Logf("Temps total de lecture (série): %v", dureeGet) + t.Logf("Temps moyen de lecture: %v/opération", dureeGet/100) + t.Logf("Ratio hits/misses: %d/%d", hits, misses) + + // Test de lecture en parallèle + debut = time.Now() + var wg sync.WaitGroup + errChan := make(chan error, len(donnees)) + + for _, d := range donnees { + wg.Add(1) + go func(url string) { + defer wg.Done() + _, exists, err := cache.Get(ctx, url) + if err != nil { + errChan <- fmt.Errorf("erreur lecture parallèle: %v", err) + } + if !exists { + errChan <- fmt.Errorf("donnée non trouvée: %s", url) + } + }(d.url) + } + + wg.Wait() + close(errChan) + dureeGetParallele := time.Since(debut) + t.Logf("Temps total de lecture (parallèle): %v", dureeGetParallele) + + // Vérifier les erreurs de lecture parallèle + for err := range errChan { + t.Errorf("Erreur pendant la lecture parallèle: %v", err) + } + + // Critères de performance plus réalistes + tempsMaxSet := 5 * time.Millisecond // 5ms max par opération de mise en cache + tempsMaxGet := 1 * time.Millisecond // 1ms max par opération de lecture + + if dureeSet/100 > tempsMaxSet { + t.Errorf("Performance SET insuffisante: %v/op (max attendu: %v/op)", dureeSet/100, tempsMaxSet) + } + if dureeGet/100 > tempsMaxGet { + t.Errorf("Performance GET insuffisante: %v/op (max attendu: %v/op)", dureeGet/100, tempsMaxGet) + } + if hits != len(donnees) { + t.Errorf("Certaines données n'ont pas été trouvées dans le cache: %d hits sur %d attendus", hits, len(donnees)) + } + }) + + t.Run("Test gestion de la charge", func(t *testing.T) { + // Simuler des accès concurrents + const nbGoroutines = 100 + const nbRequetesParGoroutine = 1000 + + errChan := make(chan error, nbGoroutines) + done := make(chan bool, nbGoroutines) + + for i := 0; i < nbGoroutines; i++ { + go func(id int) { + for j := 0; j < nbRequetesParGoroutine; j++ { + key := fmt.Sprintf("/charge-test-%d-%d", id, j) + err := cache.Set(ctx, key, []byte("test"), nil, time.Hour) + if err != nil { + errChan <- fmt.Errorf("goroutine %d: %v", id, err) + return + } + + _, _, err = cache.Get(ctx, key) + if err != nil { + errChan <- fmt.Errorf("goroutine %d: %v", id, err) + return + } + } + done <- true + }(i) + } + + // Attendre la fin des goroutines + for i := 0; i < nbGoroutines; i++ { + select { + case err := <-errChan: + t.Errorf("Erreur pendant le test de charge: %v", err) + case <-done: + // OK + case <-time.After(30 * time.Second): + t.Error("Timeout pendant le test de charge") + } + } + }) + + t.Run("Test nettoyage automatique", func(t *testing.T) { + // Remplir le cache avec des entrées qui expirent rapidement + for i := 0; i < 50; i++ { + key := fmt.Sprintf("/expire-test-%d", i) + err := cache.Set(ctx, key, []byte("test"), nil, time.Millisecond) + if err != nil { + t.Fatalf("Erreur mise en cache: %v", err) + } + } + + // Attendre que les entrées expirent + time.Sleep(time.Millisecond * 10) + + // Vérifier que les entrées sont bien nettoyées + var entreesExpirees int + for i := 0; i < 50; i++ { + key := fmt.Sprintf("/expire-test-%d", i) + _, exists, _ := cache.Get(ctx, key) + if exists { + entreesExpirees++ + } + } + + if entreesExpirees > 0 { + t.Errorf("%d entrées expirées toujours en cache", entreesExpirees) + } + }) +} diff --git a/app/cdn/internal/loadbalancer/loadbalancer.go b/app/cdn/internal/loadbalancer/loadbalancer.go new file mode 100644 index 0000000..ebaa957 --- /dev/null +++ b/app/cdn/internal/loadbalancer/loadbalancer.go @@ -0,0 +1,361 @@ +package loadbalancer + +import ( + "context" + "errors" + "net/http" + "sync" + "sync/atomic" + "time" + + "app/internal/metrics" + "github.com/sirupsen/logrus" +) + +// Backend représente un serveur backend avec ses propriétés +type Backend struct { + URL string // URL du serveur backend + Weight int // Poids pour l'algorithme weighted round-robin + CurrentWeight int // Poids actuel (utilisé dans l'algorithme weighted round-robin) + Connections int32 // Nombre de connexions actives + IsAlive bool // État de santé du backend + LastCheck time.Time // Dernière vérification de santé + FailCount int // Nombre d'échecs consécutifs + mu sync.RWMutex +} + +// LoadBalancerMetrics contient les métriques de performance +type LoadBalancerMetrics struct { + TotalRequests uint64 + FailedRequests uint64 + ActiveBackends int32 + AverageLatency float64 + RequestsPerBackend map[string]*uint64 +} + +// LoadBalancer définit l'interface commune pour toutes les stratégies +type LoadBalancer interface { + // NextBackend retourne le prochain backend à utiliser + NextBackend(ctx context.Context) (*Backend, error) + + // HealthCheck vérifie l'état de santé des backends + HealthCheck(ctx context.Context) error + + // GetMetrics retourne les métriques du load balancer + GetMetrics() *LoadBalancerMetrics + + // Close nettoie les ressources du load balancer + Close() error +} + +// Configuration commune pour tous les load balancers +type Config struct { + HealthCheckInterval time.Duration + HealthCheckTimeout time.Duration + MaxFailCount int + RetryTimeout time.Duration +} + +// Erreurs communes du load balancer +var ( + ErrNoAvailableBackends = errors.New("aucun backend disponible") +) + +// RoundRobin implémente la stratégie de répartition cyclique +type RoundRobin struct { + backends []*Backend + current uint32 + config Config + metrics LoadBalancerMetrics + client *http.Client + mu sync.RWMutex +} + +// NewRoundRobin crée une nouvelle instance de RoundRobin +func NewRoundRobin(urls []string, config Config) *RoundRobin { + backends := make([]*Backend, len(urls)) + for i, url := range urls { + backends[i] = &Backend{ + URL: url, + IsAlive: true, + FailCount: 0, + } + } + + lb := &RoundRobin{ + backends: backends, + config: config, + metrics: LoadBalancerMetrics{ + RequestsPerBackend: make(map[string]*uint64), + ActiveBackends: int32(len(urls)), // Initialisation du nombre de backends actifs + }, + client: &http.Client{ + Timeout: config.HealthCheckTimeout, + }, + } + + // Démarrage des health checks périodiques + go lb.healthCheckLoop() + + return lb +} + +func (r *RoundRobin) healthCheckLoop() { + ticker := time.NewTicker(r.config.HealthCheckInterval) + defer ticker.Stop() + + for range ticker.C { + // logrus.Info("Démarrage de la vérification de santé des backends") + if err := r.HealthCheck(context.Background()); err != nil { + logrus.WithError(err).Error("Erreur lors de la vérification de santé") + } + } +} + +func (r *RoundRobin) checkBackendHealth(ctx context.Context, backend *Backend) { + backend.mu.Lock() + defer backend.mu.Unlock() + + logrus.WithFields(logrus.Fields{ + "backend_url": backend.URL, + "timestamp": time.Now().Format(time.RFC3339), + }).Debug("Vérification de la santé du backend") + + req, err := http.NewRequestWithContext(ctx, "GET", backend.URL+"/health", nil) + if err != nil { + logrus.WithError(err).WithField("backend_url", backend.URL).Error("Erreur lors de la création de la requête health check") + backend.IsAlive = false + backend.FailCount++ + return + } + + resp, err := r.client.Do(req) + if err != nil { + logrus.WithError(err).WithField("backend_url", backend.URL).Error("Échec du health check") + backend.IsAlive = false + backend.FailCount++ + return + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + logrus.WithFields(logrus.Fields{ + "backend_url": backend.URL, + "status": resp.StatusCode, + }).Debug("Backend en bonne santé") + backend.IsAlive = true + backend.FailCount = 0 + } else { + logrus.WithFields(logrus.Fields{ + "backend_url": backend.URL, + "status": resp.StatusCode, + }).Warn("Backend en mauvaise santé") + backend.IsAlive = false + backend.FailCount++ + } + + backend.LastCheck = time.Now() +} + +func (r *RoundRobin) HealthCheck(ctx context.Context) error { + var wg sync.WaitGroup + for _, backend := range r.backends { + wg.Add(1) + go func(b *Backend) { + defer wg.Done() + r.checkBackendHealth(ctx, b) + }(backend) + } + wg.Wait() + + activeCount := int32(0) + for _, backend := range r.backends { + backend.mu.RLock() + if backend.IsAlive { + activeCount++ + } + backend.mu.RUnlock() + } + atomic.StoreInt32(&r.metrics.ActiveBackends, activeCount) + metrics.UpdateActiveBackends(activeCount) + + return nil +} + +func (r *RoundRobin) NextBackend(ctx context.Context) (*Backend, error) { + atomic.AddUint64(&r.metrics.TotalRequests, 1) + + r.mu.RLock() + defer r.mu.RUnlock() + + start := atomic.LoadUint32(&r.current) + next := start + maxTries := len(r.backends) + + for i := 0; i < maxTries; i++ { + next = (next + 1) % uint32(len(r.backends)) + backend := r.backends[next] + + backend.mu.RLock() + isAlive := backend.IsAlive + backend.mu.RUnlock() + + if isAlive { + atomic.StoreUint32(&r.current, next) + if _, ok := r.metrics.RequestsPerBackend[backend.URL]; !ok { + r.metrics.RequestsPerBackend[backend.URL] = new(uint64) + } + atomic.AddUint64(r.metrics.RequestsPerBackend[backend.URL], 1) + return backend, nil + } + } + + atomic.AddUint64(&r.metrics.FailedRequests, 1) + return nil, ErrNoAvailableBackends +} + +func (r *RoundRobin) GetMetrics() *LoadBalancerMetrics { + r.mu.RLock() + defer r.mu.RUnlock() + + metrics := &LoadBalancerMetrics{ + TotalRequests: atomic.LoadUint64(&r.metrics.TotalRequests), + FailedRequests: atomic.LoadUint64(&r.metrics.FailedRequests), + ActiveBackends: atomic.LoadInt32(&r.metrics.ActiveBackends), + AverageLatency: r.metrics.AverageLatency, + RequestsPerBackend: make(map[string]*uint64), + } + + for k, v := range r.metrics.RequestsPerBackend { + metrics.RequestsPerBackend[k] = new(uint64) + *metrics.RequestsPerBackend[k] = atomic.LoadUint64(v) + } + + return metrics +} + +func (r *RoundRobin) Close() error { + r.mu.Lock() + defer r.mu.Unlock() + + // Arrêter les vérifications de santé et nettoyer les ressources + if r.client != nil { + r.client.CloseIdleConnections() + } + + // Réinitialiser les backends + for _, backend := range r.backends { + backend.IsAlive = false + backend.CurrentWeight = 0 + backend.Connections = 0 + } + + return nil +} + +// WeightedRoundRobin hérite des fonctionnalités de base de RoundRobin +type WeightedRoundRobin struct { + *RoundRobin +} + +func NewWeightedRoundRobin(urls []string, weights []int, config Config) *WeightedRoundRobin { + rr := NewRoundRobin(urls, config) + for i, weight := range weights { + rr.backends[i].Weight = weight + rr.backends[i].CurrentWeight = weight + } + return &WeightedRoundRobin{RoundRobin: rr} +} + +func (w *WeightedRoundRobin) NextBackend(ctx context.Context) (*Backend, error) { + atomic.AddUint64(&w.metrics.TotalRequests, 1) + + w.mu.Lock() + defer w.mu.Unlock() + + var best *Backend + var totalWeight int + + for _, b := range w.backends { + b.mu.RLock() + isAlive := b.IsAlive + b.mu.RUnlock() + + if !isAlive { + continue + } + + b.CurrentWeight += b.Weight + totalWeight += b.Weight + if best == nil || b.CurrentWeight > best.CurrentWeight { + best = b + } + } + + if best == nil { + atomic.AddUint64(&w.metrics.FailedRequests, 1) + return nil, ErrNoAvailableBackends + } + + best.CurrentWeight -= totalWeight + if _, ok := w.metrics.RequestsPerBackend[best.URL]; !ok { + w.metrics.RequestsPerBackend[best.URL] = new(uint64) + } + atomic.AddUint64(w.metrics.RequestsPerBackend[best.URL], 1) + return best, nil +} + +func (w *WeightedRoundRobin) Close() error { + return w.RoundRobin.Close() +} + +// LeastConnections hérite également des fonctionnalités de base +type LeastConnections struct { + *RoundRobin +} + +func NewLeastConnections(urls []string, config Config) *LeastConnections { + return &LeastConnections{RoundRobin: NewRoundRobin(urls, config)} +} + +func (l *LeastConnections) NextBackend(ctx context.Context) (*Backend, error) { + atomic.AddUint64(&l.metrics.TotalRequests, 1) + + l.mu.RLock() + defer l.mu.RUnlock() + + var best *Backend + var minConn int32 = -1 + + for _, b := range l.backends { + b.mu.RLock() + isAlive := b.IsAlive + connections := atomic.LoadInt32(&b.Connections) + b.mu.RUnlock() + + if !isAlive { + continue + } + + if minConn == -1 || connections < minConn { + minConn = connections + best = b + } + } + + if best == nil { + atomic.AddUint64(&l.metrics.FailedRequests, 1) + return nil, ErrNoAvailableBackends + } + + atomic.AddInt32(&best.Connections, 1) + if _, ok := l.metrics.RequestsPerBackend[best.URL]; !ok { + l.metrics.RequestsPerBackend[best.URL] = new(uint64) + } + atomic.AddUint64(l.metrics.RequestsPerBackend[best.URL], 1) + return best, nil +} + +func (l *LeastConnections) Close() error { + return l.RoundRobin.Close() +} diff --git a/app/cdn/internal/loadbalancer/loadbalancer_test.go b/app/cdn/internal/loadbalancer/loadbalancer_test.go new file mode 100644 index 0000000..f2cbfb2 --- /dev/null +++ b/app/cdn/internal/loadbalancer/loadbalancer_test.go @@ -0,0 +1,202 @@ +package loadbalancer + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func setupTestServers(t *testing.T) ([]*httptest.Server, []string) { + servers := make([]*httptest.Server, 3) + urls := make([]string, 3) + + for i := 0; i < 3; i++ { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/health" { + w.WriteHeader(http.StatusOK) + return + } + w.WriteHeader(http.StatusOK) + })) + servers[i] = server + urls[i] = server.URL + } + + return servers, urls +} + +func TestRoundRobin(t *testing.T) { + servers, urls := setupTestServers(t) + defer func() { + for _, server := range servers { + server.Close() + } + }() + + config := Config{ + HealthCheckInterval: time.Second, + HealthCheckTimeout: time.Second, + MaxFailCount: 3, + RetryTimeout: time.Second, + } + + lb := NewRoundRobin(urls, config) + ctx := context.Background() + + // Test distribution équitable + counts := make(map[string]int) + for i := 0; i < 300; i++ { + backend, err := lb.NextBackend(ctx) + if err != nil { + t.Fatalf("Erreur inattendue: %v", err) + } + counts[backend.URL]++ + } + + // Vérification de la distribution + for _, count := range counts { + if count < 95 || count > 105 { + t.Errorf("Distribution non équitable: %v", counts) + } + } + + // Test des métriques + metrics := lb.GetMetrics() + if metrics.TotalRequests != 300 { + t.Errorf("Nombre total de requêtes incorrect: %d", metrics.TotalRequests) + } +} + +func TestWeightedRoundRobin(t *testing.T) { + servers, urls := setupTestServers(t) + defer func() { + for _, server := range servers { + server.Close() + } + }() + + weights := []int{1, 2, 3} // Le dernier serveur devrait recevoir 3x plus de requêtes + config := Config{ + HealthCheckInterval: time.Second, + HealthCheckTimeout: time.Second, + MaxFailCount: 3, + RetryTimeout: time.Second, + } + + lb := NewWeightedRoundRobin(urls, weights, config) + ctx := context.Background() + + counts := make(map[string]int) + for i := 0; i < 600; i++ { + backend, err := lb.NextBackend(ctx) + if err != nil { + t.Fatalf("Erreur inattendue: %v", err) + } + counts[backend.URL]++ + } + + // Vérification des ratios + total := float64(counts[urls[0]] + counts[urls[1]] + counts[urls[2]]) + ratios := make(map[string]float64) + for url, count := range counts { + ratios[url] = float64(count) / total + } + + expectedRatios := map[int]float64{ + 0: 1.0 / 6.0, // Weight 1 + 1: 2.0 / 6.0, // Weight 2 + 2: 3.0 / 6.0, // Weight 3 + } + + for i, url := range urls { + expected := expectedRatios[i] + actual := ratios[url] + if actual < expected-0.05 || actual > expected+0.05 { + t.Errorf("Ratio incorrect pour %s: attendu %.2f, obtenu %.2f", url, expected, actual) + } + } +} + +func TestLeastConnections(t *testing.T) { + servers, urls := setupTestServers(t) + defer func() { + for _, server := range servers { + server.Close() + } + }() + + config := Config{ + HealthCheckInterval: time.Second, + HealthCheckTimeout: time.Second, + MaxFailCount: 3, + RetryTimeout: time.Second, + } + + lb := NewLeastConnections(urls, config) + ctx := context.Background() + + // Simulation de connexions actives + backend1, _ := lb.NextBackend(ctx) + backend1.Connections = 5 + backend2, _ := lb.NextBackend(ctx) + backend2.Connections = 2 + backend3, _ := lb.NextBackend(ctx) + backend3.Connections = 8 + + // Le backend avec le moins de connexions devrait être choisi + chosen, err := lb.NextBackend(ctx) + if err != nil { + t.Fatalf("Erreur inattendue: %v", err) + } + + if chosen.URL != backend2.URL { + t.Errorf("Le mauvais backend a été choisi: attendu %s, obtenu %s", backend2.URL, chosen.URL) + } +} + +func TestHealthCheck(t *testing.T) { + servers, urls := setupTestServers(t) + defer func() { + for _, server := range servers { + server.Close() + } + }() + + config := Config{ + HealthCheckInterval: 100 * time.Millisecond, + HealthCheckTimeout: time.Second, + MaxFailCount: 2, + RetryTimeout: time.Second, + } + + lb := NewRoundRobin(urls, config) + ctx := context.Background() + + // Arrêt d'un serveur + servers[1].Close() + + // Attente que le health check détecte le serveur mort + time.Sleep(300 * time.Millisecond) + + // Vérification que le serveur mort n'est pas sélectionné + for i := 0; i < 100; i++ { + backend, err := lb.NextBackend(ctx) + if err != nil { + t.Fatalf("Erreur inattendue: %v", err) + } + if backend.URL == urls[1] { + t.Error("Un serveur mort a été sélectionné") + } + } + + // Vérification des métriques + metrics := lb.GetMetrics() + //print metrics + fmt.Println(metrics) + if metrics.ActiveBackends != 2 { + t.Errorf("Nombre incorrect de backends actifs: %d", metrics.ActiveBackends) + } +} diff --git a/app/cdn/internal/metrics/prometheus.go b/app/cdn/internal/metrics/prometheus.go new file mode 100644 index 0000000..eb0deef --- /dev/null +++ b/app/cdn/internal/metrics/prometheus.go @@ -0,0 +1,137 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + // Métriques du cache + CacheHits = promauto.NewCounter(prometheus.CounterOpts{ + Name: "cdn_cache_hits_total", + Help: "Nombre total de hits du cache", + }) + + CacheMisses = promauto.NewCounter(prometheus.CounterOpts{ + Name: "cdn_cache_misses_total", + Help: "Nombre total de misses du cache", + }) + + CacheSize = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "cdn_cache_size_bytes", + Help: "Taille totale du cache en bytes", + }) + + // Métriques du load balancer + BackendRequests = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "cdn_backend_requests_total", + Help: "Nombre total de requêtes par backend", + }, []string{"backend_url"}) + + BackendLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "cdn_backend_latency_seconds", + Help: "Latence des requêtes par backend", + Buckets: []float64{0.01, 0.05, 0.1, 0.5, 1, 2, 5}, + }, []string{"backend_url"}) + + BackendErrors = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "cdn_backend_errors_total", + Help: "Nombre total d'erreurs par backend", + }, []string{"backend_url", "error_type"}) + + ActiveBackends = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "cdn_active_backends", + Help: "Nombre de backends actifs", + }) + + // Métriques de sécurité + RateLimitExceeded = promauto.NewCounter(prometheus.CounterOpts{ + Name: "cdn_rate_limit_exceeded_total", + Help: "Nombre total de requêtes ayant dépassé la limite de taux", + }) + + DDoSAttempts = promauto.NewCounter(prometheus.CounterOpts{ + Name: "cdn_ddos_attempts_total", + Help: "Nombre total de tentatives de DDoS détectées", + }) + + // Métriques HTTP + HttpRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "cdn_http_requests_total", + Help: "Nombre total de requêtes HTTP", + }, []string{"method", "path", "status"}) + + HttpRequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "cdn_http_request_duration_seconds", + Help: "Durée des requêtes HTTP", + Buckets: prometheus.DefBuckets, + }, []string{"method", "path"}) + + HttpResponseSize = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "cdn_http_response_size_bytes", + Help: "Taille des réponses HTTP en bytes", + Buckets: []float64{100, 1000, 10000, 100000, 1000000}, + }, []string{"method", "path"}) + + // Métriques système + CpuUsage = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "cdn_cpu_usage_percent", + Help: "Utilisation CPU en pourcentage", + }) + + MemoryUsage = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "cdn_memory_usage_bytes", + Help: "Utilisation mémoire en bytes", + }) + + OpenConnections = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "cdn_open_connections", + Help: "Nombre de connexions ouvertes", + }) +) + +// RecordRequest enregistre les métriques d'une requête HTTP +func RecordRequest(method, path string, status int, duration float64, size int64) { + HttpRequestsTotal.WithLabelValues(method, path, string(status)).Inc() + HttpRequestDuration.WithLabelValues(method, path).Observe(duration) + HttpResponseSize.WithLabelValues(method, path).Observe(float64(size)) +} + +// RecordBackendRequest enregistre les métriques d'une requête backend +func RecordBackendRequest(backendURL string, duration float64, err error) { + BackendRequests.WithLabelValues(backendURL).Inc() + BackendLatency.WithLabelValues(backendURL).Observe(duration) + + if err != nil { + BackendErrors.WithLabelValues(backendURL, err.Error()).Inc() + } +} + +// UpdateCacheMetrics met à jour les métriques du cache +func UpdateCacheMetrics(hits, misses uint64, size int64) { + CacheHits.Add(float64(hits)) + CacheMisses.Add(float64(misses)) + CacheSize.Set(float64(size)) +} + +// UpdateSystemMetrics met à jour les métriques système +func UpdateSystemMetrics(cpuPercent float64, memoryBytes int64, connections int) { + CpuUsage.Set(cpuPercent) + MemoryUsage.Set(float64(memoryBytes)) + OpenConnections.Set(float64(connections)) +} + +// UpdateActiveBackends met à jour le nombre de backends actifs +func UpdateActiveBackends(count int32) { + ActiveBackends.Set(float64(count)) +} + +// RecordSecurityEvent enregistre les événements de sécurité +func RecordSecurityEvent(eventType string) { + switch eventType { + case "rate_limit": + RateLimitExceeded.Inc() + case "ddos": + DDoSAttempts.Inc() + } +} diff --git a/app/CDN/internal/middleware/middleware.go b/app/cdn/internal/middleware/middleware.go similarity index 100% rename from app/CDN/internal/middleware/middleware.go rename to app/cdn/internal/middleware/middleware.go diff --git a/app/cdn/internal/middleware/security.go b/app/cdn/internal/middleware/security.go new file mode 100644 index 0000000..a566c26 --- /dev/null +++ b/app/cdn/internal/middleware/security.go @@ -0,0 +1,72 @@ +package middleware + +import ( + "golang.org/x/time/rate" + "net/http" + "sync" +) + +// RateLimiter implémente la protection contre les attaques DDoS +// en limitant le nombre de requêtes par IP +type RateLimiter struct { + visitors map[string]*rate.Limiter + mu sync.RWMutex + rate rate.Limit + burst int +} + +// NewRateLimiter crée un nouveau limiteur de taux avec un taux et un burst spécifiés +func NewRateLimiter(r rate.Limit, b int) *RateLimiter { + return &RateLimiter{ + visitors: make(map[string]*rate.Limiter), + rate: r, + burst: b, + } +} + +// getLimiter retourne le rate limiter pour une IP donnée +func (rl *RateLimiter) getLimiter(ip string) *rate.Limiter { + rl.mu.Lock() + defer rl.mu.Unlock() + + limiter, exists := rl.visitors[ip] + if !exists { + limiter = rate.NewLimiter(rl.rate, rl.burst) + rl.visitors[ip] = limiter + } + + return limiter +} + +// RateLimit est un middleware qui limite le taux de requêtes par IP +func (rl *RateLimiter) RateLimit(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ip := r.RemoteAddr + limiter := rl.getLimiter(ip) + + if !limiter.Allow() { + http.Error(w, "Too many requests", http.StatusTooManyRequests) + return + } + + next.ServeHTTP(w, r) + }) +} + +// SecurityHeaders ajoute des en-têtes de sécurité à la réponse +func SecurityHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Protection XSS + w.Header().Set("X-XSS-Protection", "1; mode=block") + // Protection contre le clickjacking + w.Header().Set("X-Frame-Options", "DENY") + // Protection contre le MIME sniffing + w.Header().Set("X-Content-Type-Options", "nosniff") + // HSTS (forcer HTTPS) + w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") + // CSP + w.Header().Set("Content-Security-Policy", "default-src 'self'") + + next.ServeHTTP(w, r) + }) +} diff --git a/app/cdn/main.go b/app/cdn/main.go new file mode 100644 index 0000000..f4b0eec --- /dev/null +++ b/app/cdn/main.go @@ -0,0 +1,252 @@ +package main + +import ( + "app/internal/cache" + "app/internal/loadbalancer" + "app/internal/metrics" + "app/internal/middleware" // Ajout de l'import du package api + "context" + "fmt" + "io" + "net/http" + "os" + "os/signal" + "strconv" + "syscall" + "time" + + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/sirupsen/logrus" + "golang.org/x/time/rate" +) + +var log = logrus.New() + +func init() { + // Configuration de logrus + log.SetFormatter(&logrus.JSONFormatter{ + TimestampFormat: time.RFC3339, + }) + log.SetOutput(os.Stdout) + log.SetLevel(logrus.InfoLevel) +} + +func main() { + // Configuration du cache avec une taille maximale de 1000 entrées + memCache, err := cache.NewMemoryCache(1000) + if err != nil { + log.WithError(err).Fatal("Erreur initialisation cache") + } + + // Configuration du Load Balancer en mode Weighted Round Robin + backends := []string{"http://backend:8080"} + weights := []int{1} // Un seul poids pour un seul backend + lb := loadbalancer.NewWeightedRoundRobin(backends, weights, loadbalancer.Config{ + HealthCheckInterval: 15 * time.Second, + HealthCheckTimeout: time.Second, + MaxFailCount: 3, + RetryTimeout: time.Second, + }) + + // Configuration du Rate Limiter (1000 requêtes par minute par IP) + rateLimiter := middleware.NewRateLimiter(rate.Limit(100/60.0), 100) + + // Configuration du routeur HTTP + mux := http.NewServeMux() + + // Endpoint de monitoring + mux.Handle("/metrics", promhttp.Handler()) + + // Health check endpoints + mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("healthy")) + }) + + mux.HandleFunc("/ready", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("ready")) + }) + + // Endpoint pour vider le cache + mux.HandleFunc("/cache/purge", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + memCache.Clear() + log.Info("Cache vidé avec succès") + w.WriteHeader(http.StatusOK) + w.Write([]byte("Cache purgé")) + }) + + // Route principale avec middleware de sécurité + mainHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + requestID := fmt.Sprintf("%d", time.Now().UnixNano()) + requestIDInt, _ := strconv.ParseInt(requestID, 10, 64) + + // Logger la requête entrante + log.WithFields(logrus.Fields{ + "request_id": requestID, + "method": r.Method, + "path": r.URL.Path, + "client_ip": r.RemoteAddr, + }).Info("Requête entrante reçue") + + // Vérification du cache uniquement pour les requêtes GET + if r.Method == http.MethodGet { + // Création d'une clé de cache unique qui inclut l'authentification + cacheKey := r.URL.Path + if auth := r.Header.Get("Authorization"); auth != "" { + // On ajoute un hash de l'authentification à la clé + cacheKey = fmt.Sprintf("%s:%s", cacheKey, auth) + } + + if cachedResponse, found, err := memCache.Get(r.Context(), cacheKey); err == nil && found { + // Vérifier si la réponse en cache contient des informations d'authentification + if auth, ok := cachedResponse.Headers["auth"]; ok && auth == r.Header.Get("Authorization") { + metrics.CacheHits.Inc() + log.WithFields(logrus.Fields{ + "request_id": requestID, + "path": r.URL.Path, + "source": "cache", + }).Info("Réponse servie depuis le cache") + w.Write(cachedResponse.Value.([]byte)) + return + } + } + metrics.CacheMisses.Inc() + } + + // Sélection du backend + backend, err := lb.NextBackend(r.Context()) + if err != nil { + metrics.RecordRequest(r.Method, r.URL.Path, http.StatusServiceUnavailable, time.Since(start).Seconds(), 0) + log.WithFields(logrus.Fields{ + "request_id": requestID, + "error": err, + }).Error("Aucun backend disponible") + http.Error(w, "No backend available", http.StatusServiceUnavailable) + return + } + + // Créer une nouvelle requête pour le backend + backendReq, err := http.NewRequestWithContext(r.Context(), r.Method, backend.URL+r.URL.Path, r.Body) + if err != nil { + metrics.RecordRequest(r.Method, r.URL.Path, http.StatusBadGateway, time.Since(start).Seconds(), 0) + log.WithFields(logrus.Fields{ + "request_id": requestID, + "error": err, + }).Error("Erreur création requête backend") + http.Error(w, "Backend error", http.StatusBadGateway) + return + } + + // Copier les headers + for k, v := range r.Header { + backendReq.Header[k] = v + } + + // Envoyer la requête au backend + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(backendReq) + if err != nil { + metrics.RecordBackendRequest(backend.URL, time.Since(start).Seconds(), err) + log.WithFields(logrus.Fields{ + "request_id": requestID, + "backend": backend.URL, + "error": err, + }).Error("Erreur requête backend") + http.Error(w, "Backend error", http.StatusBadGateway) + return + } + defer resp.Body.Close() + + // Copier la réponse + body, err := io.ReadAll(resp.Body) + if err != nil { + metrics.RecordRequest(r.Method, r.URL.Path, http.StatusInternalServerError, time.Since(start).Seconds(), 0) + log.WithFields(logrus.Fields{ + "request_id": requestID, + "error": err, + }).Error("Erreur lecture réponse") + http.Error(w, "Error reading response", http.StatusInternalServerError) + return + } + + // Mettre en cache si c'est une requête GET + if r.Method == http.MethodGet && resp.StatusCode == http.StatusOK { + cacheKey := r.URL.Path + metadata := map[string]string{} + + // Si la requête est authentifiée, on stocke l'authentification dans les métadonnées + if auth := r.Header.Get("Authorization"); auth != "" { + cacheKey = fmt.Sprintf("%s:%s", cacheKey, auth) + metadata["auth"] = auth + } + + if err := memCache.Set(r.Context(), cacheKey, body, metadata, 1*time.Hour); err != nil { + log.WithFields(logrus.Fields{ + "request_id": requestID, + "error": err, + }).Error("Erreur mise en cache") + } + } + + // Copier les headers de réponse + for k, v := range resp.Header { + w.Header()[k] = v + } + w.WriteHeader(resp.StatusCode) + w.Write(body) + + // Enregistrer les métriques finales + metrics.RecordRequest(r.Method, r.URL.Path, resp.StatusCode, time.Since(start).Seconds(), int64(len(body))) + metrics.RecordBackendRequest(backend.URL, time.Since(start).Seconds(), nil) + + log.WithFields(logrus.Fields{ + "request_id": requestID, + "status_code": resp.StatusCode, + "backend": backend.URL, + "elapsed_time": time.Since(time.Unix(0, requestIDInt)).String(), + }).Info("Requête terminée") + }) + + // Application des middlewares + handler := middleware.SecurityHeaders(rateLimiter.RateLimit(mainHandler)) + mux.Handle("/", handler) + + // Configuration du serveur HTTP + srv := &http.Server{ + Addr: ":8080", + Handler: mux, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 120 * time.Second, + } + + // Démarrage du serveur dans une goroutine + go func() { + log.Printf("Serveur démarré sur le port %s", srv.Addr) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.WithError(err).Fatal("Erreur lors du démarrage du serveur") + } + }() + + // Canal pour les signaux d'arrêt + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGTERM) + + <-stop + log.Info("Arrêt du serveur...") + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + log.WithError(err).Error("Erreur lors de l'arrêt du serveur") + } + + log.Info("Serveur arrêté avec succès") +} diff --git a/app/front/Dockerfile b/app/front/Dockerfile index 207bf93..1271671 100644 --- a/app/front/Dockerfile +++ b/app/front/Dockerfile @@ -1,22 +1,32 @@ -FROM node:20-alpine AS development-dependencies-env -COPY . /app +# Étape de build +FROM node:20-alpine AS builder + +# Définir le répertoire de travail WORKDIR /app + +# Copier les fichiers de dépendances +COPY package.json package-lock.json ./ + +# Installer les dépendances RUN npm ci -FROM node:20-alpine AS production-dependencies-env -COPY ./package.json package-lock.json /app/ -WORKDIR /app -RUN npm ci --omit=dev +# Copier le reste des fichiers du projet +COPY . . -FROM node:20-alpine AS build-env -COPY . /app/ -COPY --from=development-dependencies-env /app/node_modules /app/node_modules -WORKDIR /app +# Builder l'application RUN npm run build -FROM node:20-alpine -COPY ./package.json package-lock.json /app/ -COPY --from=production-dependencies-env /app/node_modules /app/node_modules -COPY --from=build-env /app/build /app/build -WORKDIR /app -CMD ["npm", "run", "start"] \ No newline at end of file +# Étape de production +FROM nginx:alpine + +# Copier la configuration nginx personnalisée si nécessaire +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copier les fichiers buildés depuis l'étape précédente +COPY --from=builder /app/dist /usr/share/nginx/html + +# Exposer le port 80 +EXPOSE 80 + +# Démarrer nginx +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/app/front/app/app.css b/app/front/app/app.css deleted file mode 100644 index 99345d8..0000000 --- a/app/front/app/app.css +++ /dev/null @@ -1,15 +0,0 @@ -@import "tailwindcss"; - -@theme { - --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; -} - -html, -body { - @apply bg-white dark:bg-gray-950; - - @media (prefers-color-scheme: dark) { - color-scheme: dark; - } -} diff --git a/app/front/app/root.tsx b/app/front/app/root.tsx deleted file mode 100644 index 9fc6636..0000000 --- a/app/front/app/root.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { - isRouteErrorResponse, - Links, - Meta, - Outlet, - Scripts, - ScrollRestoration, -} from "react-router"; - -import type { Route } from "./+types/root"; -import "./app.css"; - -export const links: Route.LinksFunction = () => [ - { rel: "preconnect", href: "https://fonts.googleapis.com" }, - { - rel: "preconnect", - href: "https://fonts.gstatic.com", - crossOrigin: "anonymous", - }, - { - rel: "stylesheet", - href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", - }, -]; - -export function Layout({ children }: { children: React.ReactNode }) { - return ( - -
- - - -{details}
- {stack && ( -
- {stack}
-
- )}
- {error}
} ++ Glissez-déposez vos fichiers ici, ou{' '} + +
+| + Nom + | ++ Type + | ++ Taille + | ++ Dernière modification + | ++ Actions + | +
|---|---|---|---|---|
|
+
+
+ |
+ + + {file.type} + + | ++ + {file.size} + + | ++ + {file.lastModified} + + | ++ + | +
+ Ou{' '} + + créez un nouveau compte + +
++ Ou{' '} + + connectez-vous à votre compte existant + +
+