#! /usr/bin/env python
# -*- coding:utf-8 -*-

"""
Script de détection de fichiers potentiellement copyrightés dans les
répertoires des adhérents. Les tests se basent sur le nom et la taille
des fichiers.

Copyright (C) Nicolas Bruot
Licence : GPL v2
"""

import sys
import os
import re
import time
import shutil

# Variables générales

sendto = 'bureau@lists.crans.org'
analysed_path = '/home-adh/'

old_path = '/usr/scripts/var/copyrighted-content.old'
new_path = '/usr/scripts/var/copyrighted-content.new'
diffs_path = '/usr/scripts/var/copyrighted-content.diffs'
alert_path = '/usr/scripts/var/copyrighted-content.alert'

# ======================================================================
# Variables pour le calcul du score.
# ==================================

# Score minimum pour être enregistré dans le fichier des différences.
min_score = 10

# Score à partir duquel une notification est envoyée immédiatement par
# email (sans attendre l'exécution avec l'option --report).
notif_score = 40

# Taille en dessous de laquelle aucun test n'est effectué sur le
# fichier (augmenter pour améliorer la rapidité du script).
pass_size = 10e3

# Taille en dessous de laquelle seuls les filtres regexps_light sont
# testés.
light_size = 40e3

# Caractères non alphanumériques ("_" inclus)
blank = '[^a-zA-Z0-9]'
lblank = '([^a-zA-Z0-9]|^)'  # blanc ou début de chaine
rblank = '([^a-zA-Z0-9]|$)'  # blanc ou fin de chaine

# regexps, regexps_light : motifs contribuant au score du fichier.
# Pour qu'un motif contribue au score, il doit être matché et, si le
# champ min_size existe, respecter la condition de taille minimum du
# fichier.
# Si un ou plusieurs motifs avec un champ crit_size sont matchés,
# size_score est ajouté au score si la taille du fichier dépasse la
# valeur minimale des crit_size matchés.
# Seuls les motifs de regexp_light sont testés pour les fichiers d'une
# taille inférieure à light_size. Pour ce dictionnaire, il ne peut pas
# y avoir de champs min_size ou crit_size.
size_score = 10
regexps_light = {'\.torrent$': {'score':40},
                 '\.srt$': {'score':40},
                 'key.?gen': {'score':40}}
regexps = {'\.avi$': {'score':10, 'min_size':1e6, 'crit_size':1e8},
           '\.mkv$': {'score':10, 'min_size':1e6, 'crit_size':1e8},
           '\.mov$': {'score':10, 'min_size':1e6, 'crit_size':1e8},
           '\.mpeg$': {'score':10, 'min_size':1e6, 'crit_size':1e8},
           '\.mp4$': {'score':10, 'min_size':1e6, 'crit_size':1e8},
           '\.mpg$': {'score':10, 'min_size':1e6, 'crit_size':1e8},
           '\.ogm$': {'score':10, 'min_size':1e5, 'crit_size':1e8},
           '\.wmv$': {'score':10, 'min_size':1e5, 'crit_size':1e8},
           '\.flv$': {'score':10, 'min_size':1e6, 'crit_size':1e8},
           '\.mp3$': {'score':10, 'min_size':1e5},
           '\.m4a$': {'score':10, 'min_size':1e5},
           '\.zip$': {'score':5, 'min_size':1e5, 'crit_size':1e8},
           '\.gz$': {'score':3, 'min_size':1e5, 'crit_size':1e8},
           '\.rar$': {'score':5, 'min_size':1e5, 'crit_size':1e8},
           '\.dmg$': {'score':10, 'min_size':1e5, 'crit_size':1e8},
           '\.pdf$': {'score':5, 'min_size':1e5, 'crit_size':2e7},
           '\.ogg$': {'score':10, 'min_size':1e5},
           '\.iso$': {'score':15, 'min_size':1e5, 'crit_size':1e8},

           # Episodes (S01E01, Episode01...)
           's\d{1,2}e\d': {'score':20, 'min_size':1e8},
           lblank + '\d{1,2}x\d{1,2}' + rblank: {'score':20, 'min_size':1e8},
           'episode.?\d': {'score':20},
           lblank + 's\d{1,2}' + rblank: {'score':20, 'min_size':1e7},
           lblank + 'e\d{1,2}' + rblank: {'score':20, 'min_size':1e7},
           lblank + 'vol.{0,2}\d{1,2}' + rblank: {'score':20},

           # Crochets []
           '\[(?!pot).*\]': {'score':5},

           # "01 Toto.mp3"...
           '^\d{1,2}' + blank + '.*\.(mp3|ogg|wmv)$': {'score':10},

           # Autres mots-clés
           blank + 'fr' + rblank: {'score':10},
           blank + 'eng' + rblank: {'score':10},

           blank + 'vo' + rblank: {'score':10, 'min_size':1e7},
           blank + 'vost' + rblank: {'score':30},
           blank + 'vostfr' + rblank: {'score':30},
           blank + 'vosten' + rblank: {'score':30},

           blank * 3: {'score':3},

           lblank + 'dvd.?(rip|)' + rblank: {'score':20},

           lblank + 'cd' + rblank: {'score':10, 'min_size':1e8},
           lblank + 'xvid' + rblank: {'score':20},
           lblank + 'hdtv' + rblank: {'score':20},
           lblank + 'hq' + rblank: {'score':20},
           lblank + 'vtv' + rblank: {'score':20},
           lblank + 'tvrip' + rblank: {'score':20},
           lblank + 'dvtv' + rblank: {'score':20},
           lblank + 'bt' + rblank: {'score':20},
           lblank + 'fqm' + rblank: {'score':20},
           lblank + 'web.?rip' + rblank: {'score':20},
           lblank + 'natzox' + rblank: {'score':10},
           lblank + 'sfm' + rblank: {'score':20},
           lblank + 'lbp' + rblank: {'score':20},
           lblank + 'hd' + rblank: {'score':10},
           lblank + 'hdp' + rblank: {'score':10},
           lblank + 'itdk' + rblank: {'score':20},
           lblank + 'ust' + rblank: {'score':20},
           lblank + 'fxg' + rblank: {'score':20},

           lblank + 'part.?\d': {'score':10, 'min_size':1e7}}

# Si aucune expression n'a été matchée, un fichier d'une taille
# supérieure à suspect_size se verra attribuer un score de
# suspect_size_score.
suspect_size = 200e6
suspect_size_score = 30

# ======================================================================

# Pour les gros fichiers, on teste l'ensemble des expressions
# régulières :
regexps_full = dict(regexps_light, **regexps)

# Compilation des expressions régulières :
for key in regexps_light.keys():
    regexps_light[re.compile(key, re.I).findall] = regexps_light[key]
    del regexps_light[key]

for key in regexps_full.keys():
    regexps_full[re.compile(key, re.I).findall] = regexps_full[key]
    del regexps_full[key]


def human_readable(num):
    """Convertit le nombre en entrée (taille d'un fichier) en une
    chaine "human-readable". Exemple: 12000 => 12K"""

    for x in ['', 'K', 'M', 'G', 'T']:
        if num < 1000.:
            return '%3.0f%s' % (num, x)
        num /= 1000.


def get_score(file, size):
    """Calcule le score d'un fichier d'après des expressions
    régulières et des conditions de taille."""

    score = 0

    if size < light_size:
        # On ne fait que les tests simples.

        for key in regexps_light:
            m = key(file)  # liste d'expressions matchées
            param = regexps_light[key]

            score += len(m)*param['score']

        return score

    else:
        # On effectue les tests complexes.

        crit_size = 1e99
        # On parcourt les expressions régulières.
        for key in regexps_full:
            m = key(file)  # liste d'expressions matchées
            param = regexps_full[key]

            # Tests avec min_size.
            if not param.has_key('min_size') or size >= param['min_size']:
                score += len(m)*param['score']

            # Mise à jour de crit_size.
            if param.has_key('crit_size'):
                crit_size = min(crit_size, param['crit_size'])

        # Si le fichier est gros par rapport à une taille définie par les
        # expressions matchées, on augmente le score.
        if size >= crit_size:
            score += size_score

        # Si le score est nul, les fichiers gros sont quand même suspects.
        if score == 0 and size >= suspect_size:
            score = suspect_size_score

        return score


def analyse():
    """Liste les fichiers dans le dossier à analyser et ajoute les
    nouveaux fichiers à copyrighted.diffs. Envoie une notification si
    des scores trop élevés sont trouvés."""

    if os.path.isfile(new_path):
        # Rotation des fichiers
        shutil.move(new_path, old_path)
        # Lecture de old_files
        old_files = open(old_path).read().split('\n')
    else:
        old_files = []
    # Ouverture des autres fichiers
    new_file = open(new_path, 'w')
    diffs_file = open(diffs_path, 'a')
    alert_file = open(alert_path, 'w')
    os.chmod(new_path, 0600)
    os.chmod(diffs_path, 0600)
    os.chmod(alert_path, 0600)

    # Listage des fichiers    
    for root, sub_folders, files in os.walk(analysed_path):

        for file in files:
            filename = os.path.join(root, file)
            try:
                filesize = os.path.getsize(filename)
            except OSError:  # le fichier a pu être supprimé entre temps
                filesize = 0

            if filesize < pass_size:
                # On va pas tester les fichiers trop petits.
                continue

            score = get_score(file, filesize)

            if score >= min_score:
                # On inscrit le fichier.
                new_file.write(filename + '\n')
                # Si c'est un nouveau fichier, on l'ajoute au
                # fichier diffs.
                if not filename in old_files:
                    date = time.strftime("%Y-%m-%d %H:%M",
                                         time.localtime())
                    line = (str(score) + '\t' + date +
                            '\t' + human_readable(filesize) +
                            '\t' + filename + '\n')

                    diffs_file.write(line)

                    # Pour des scores vraiment élevés on va alerter
                    # par email immédiatement.
                    if score >= notif_score:
                        alert_file.write(line)

        # On ne parcourt pas les dossiers "Mail".
        if len(root.split('/')) == 3 and 'Mail' in sub_folders:
            sub_folders.remove('Mail')

    new_file.close()
    diffs_file.close()
    alert_file.close()

    # Envoi d'une notification le cas échéant.
    if os.path.getsize(alert_path) > 0:
        sujet = "Alerte de fichiers potentiellement copyrightes"
        os.system('sort -n -r ' + alert_path + ' | mail -s "' +
                  sujet + '" ' + sendto)
    os.remove(alert_path)
        

def report():
    """Envoie un email récapitulatif du fichier de
    copyrighted-content.diffs et purge le fichier."""

    # Envoi de l'email contenant diffs_path
    if os.path.isfile(diffs_path):
        sujet = "Rapport des fichiers potentiellement copyrightes"
        os.system('sort -n -r ' + diffs_path + ' | mail -s "' +
                  sujet + '" ' + sendto)
        os.remove(diffs_path)


if __name__ == '__main__':

    help = """Détection de fichiers potentiellement copyrightés.
usage: fichiers_copyrightes.py [OPTION]

OPTION doit être l'une des suivantes :

  --analyse
    Ajoute dans le fichier copyrighted-content.diffs les nouveaux
    fichiers du répertoire exploré attribués de leur score. Si des
    fichiers de score élevé sont trouvés, un email de notification est
    envoyé.

  --report
    Envoie un email récapitulatif du fichier copyrighted-content.diffs
    et purge le fichier."""

    # Gestion des options
    if len(sys.argv) != 2:
        print help
        sys.exit(0)
    elif sys.argv[1] == '--analyse':
        analyse()
    elif sys.argv[1] == '--report':
        report()
    else:
        print help
        sys.exit(0)
