Le module Duplicates identifie automatiquement les documents dont le contenu de fichier est identique, en comparant leurs checksums cryptographiques. La détection s'effectue à l'upload et peut être relancée manuellement sur l'ensemble du système.
Les doublons sont des documents composés du même fichier, jusqu'au dernier octet.
La comparaison est purement cryptographique : deux documents sont considérés comme doublons si et seulement si leurs fichiers ont le même checksum SHA-256. La similarité du contenu, du nom, des métadonnées ou du texte OCR n'est pas prise en compte.
Ce qui est comparé : le checksum du DocumentFile le plus récent de chaque document.
Ce qui n'est PAS comparé : le nom, les métadonnées, le texte extrait, ou la ressemblance visuelle.
Les backends sont des plugins auto-découverts. Deux backends sont inclus :
Label : "Exact document file checksum"
Compare le champ checksum du dernier DocumentFile de chaque document. C'est le backend principal, utilisé pour toutes les détections standard.
Label : "Exact document label"
Compare le champ label des documents (correspondance exacte). Utile pour détecter des documents portant le même nom mais pas nécessairement le même contenu.
Chaque upload de fichier déclenche automatiquement un scan :
signal_post_document_file_upload
→ handler_scan_duplicates_for()
→ task_duplicates_scan_for(document_id) [Celery, queue: duplicates]
→ Verrouillage du document
→ Comparaison checksum avec tous les autres documents
→ Mise à jour de DuplicateBackendEntry
La détection est asynchrone (worker_c) et ne bloque pas l'upload.
Pour scanner tous les documents existants (utile après import en masse ou activation du module) :
Via l'interface web :
Administration → Outils → Scanner les documents dupliqués
Déclenche task_duplicates_scan_all() qui lance un task_duplicates_scan_for() pour chaque document du système.
Via le shell Django :
from mayan.apps.duplicates.tasks import task_duplicates_scan_all
task_duplicates_scan_all.apply_async()
Quand un document est supprimé, les entrées de doublons devenues vides sont nettoyées automatiquement :
post_delete (Document)
→ handler_remove_empty_duplicates_lists()
→ task_duplicates_clean_empty_lists()
Worker : worker_c (nice=10, concurrence=4)
| Tâche | Queue | Description |
|---|---|---|
task_duplicates_scan_for(document_id) |
duplicates |
Scanne un document spécifique |
task_duplicates_scan_all() |
duplicates_slow |
Scanne tous les documents |
task_duplicates_clean_empty_lists() |
duplicates |
Nettoie les entrées vides |
task_duplicates_scan_for utilise un verrou par document (duplicates__scan_document-{id}) pour éviter les scans concurrents du même document. En cas de conflit, la tâche se relance automatiquement.
StoredDuplicateBackend (backend configuré)
└── DuplicateBackendEntry (unique par backend + document source)
├── document (FK → Document) ← le document "original"
└── documents (M2M → Document) ← ses doublons détectés
StoredDuplicateBackend — un enregistrement par backend actif (backend_path, backend_data)
DuplicateBackendEntry — la relation de doublon :
stored_backend (FK) — quel backend a détectédocument (FK) — document sourcedocuments (M2M) — documents identifiés comme doublons(stored_backend, document)Modèles proxy :
DuplicateSourceDocument — document vu comme "original"DuplicateTargetDocument — document vu comme "doublon" (ajoute le champ backend en API)| Vue | URL | Description |
|---|---|---|
| Liste des documents dupliqués | /documents/duplicated/ |
Tous les documents ayant au moins un doublon |
| Doublons d'un document | /documents/{id}/duplicates/ |
Liste des doublons d'un document spécifique |
| Lancer un scan global | /documents/duplicated/scan/ |
Déclenche le scan de tous les documents |
La vue "Documents dupliqués" est accessible depuis le menu Documents → Doublons.
La vue "Doublons d'un document" est accessible depuis la page de détail de n'importe quel document (onglet / facette "Doublons").
# Lister tous les documents ayant des doublons
curl -H "Authorization: Token <token>" \
"https://mayan.example.com/api/v4/documents/duplicated/"
# Lister les doublons d'un document spécifique
curl -H "Authorization: Token <token>" \
"https://mayan.example.com/api/v4/documents/42/duplicates/"
# Réponse — chaque doublon inclut le backend qui l'a détecté :
{
"count": 2,
"results": [
{
"id": 57,
"label": "facture_jan_2024.pdf",
"backend": "mayan.apps.duplicates.duplicate_backends.DuplicateBackendFileChecksum",
...
}
]
}
Le module Duplicates est principalement en lecture seule : il détecte et affiche les doublons, mais ne fournit pas de bouton "fusionner" ou "supprimer le doublon" intégré.
Actions disponibles :
from mayan.apps.duplicates.models import DuplicateBackendEntry
from mayan.apps.documents.models import Document
# Trouver les doublons d'un document
doc = Document.objects.get(pk=42)
entry = DuplicateBackendEntry.objects.filter(document=doc).first()
if entry:
print(f"Doublons trouvés : {entry.documents.count()}")
for dup in entry.documents.all():
print(f" - [{dup.pk}] {dup.label}")
# Supprimer un document doublon (action irréversible)
dup_to_delete = Document.objects.get(pk=57)
dup_to_delete.delete()
from mayan.apps.duplicates.models import DuplicateBackendEntry
# Lister tous les groupes de doublons
for entry in DuplicateBackendEntry.objects.prefetch_related('documents').all():
if entry.documents.exists():
print(f"\nDocument original : [{entry.document.pk}] {entry.document.label}")
for dup in entry.documents.all():
print(f" Doublon : [{dup.pk}] {dup.label} — uploadé le {dup.datetime_created}")
| Méthode | URL | Permission | Description |
|---|---|---|---|
GET |
/api/v4/documents/duplicated/ |
document_view |
Documents ayant des doublons |
GET |
/api/v4/documents/{id}/duplicates/ |
document_view |
Doublons d'un document |
Les résultats sont filtrés par les ACL de l'utilisateur : seuls les documents auxquels l'utilisateur a accès apparaissent.
| Permission | Description |
|---|---|
permission_document_view |
Consulter les listes de doublons (vue et API) |
permission_document_tools |
Déclencher un scan global |
duplicatesDocumentFile avec un checksum calculé :from mayan.apps.documents.models import Document
doc = Document.objects.get(pk=42)
print(doc.file_latest.checksum) # Doit afficher un hash SHA-256
# Via l'interface
Administration → Outils → Scanner les documents dupliqués
# Via le shell
from mayan.apps.duplicates.tasks import task_duplicates_scan_all
task_duplicates_scan_all.apply_async()
from mayan.apps.duplicates.models import DuplicateBackendEntry
from mayan.apps.duplicates.tasks import task_duplicates_scan_all
# Supprimer toutes les entrées existantes
DuplicateBackendEntry.objects.all().delete()
# Relancer le scan complet
task_duplicates_scan_all.apply_async()
| Fichier | Rôle |
|---|---|
duplicate_backends.py |
DuplicateBackendFileChecksum, DuplicateBackendLabel |
classes.py |
Classe de base DuplicateBackend, métaclasse, registre |
models.py |
StoredDuplicateBackend, DuplicateBackendEntry, proxies |
managers.py |
get_duplicated_documents(), get_duplicates_of() |
tasks.py |
task_duplicates_scan_for, task_duplicates_scan_all, task_duplicates_clean_empty_lists |
handlers.py |
Handlers signal upload et suppression |
views.py |
Vues UI (liste, détail, scan) |
api_views.py |
Endpoints REST |