Sauvegarder un site web sur S3

Cloner un site web en cours d'exécution sur S3 et CloudFront.

C05348A3-9AB8-42C9-A6E0-81DB3AC59FEB
           

L'objectif de ce script Python est de sauvegarder ou de migrer toutes les pages d'un site web en cours d'exécution vers un bucket S3, afin qu'elles soient servies par une distribution CloudFront.

Mon cas d'utilisation est simple, je veux avoir une sauvegarde peu coûteuse de mon site web au cas où le serveur web aurait des problèmes. De nos jours, l'un des moyens les plus économiques d'exploiter un site web est d'avoir un seau S3 accessible par le web. Non seulement c'est très bon marché, mais c'est aussi extrêmement évolutif.

Lorsque je parle de "seau S3 accessible par le web", bien sûr, les gens ne font plus cela (s'il vous plaît, ne le faites plus), mettez une distribution CloudFront devant (également super évolutive et peu coûteuse).

Pour démarrer cette partie, vous pouvez utiliser mon modèle CloudFormation, voir CloudFront and S3 Bucket CloudFormation Stack.

Le code

Les bibliothèques

Pour cela, nous utiliserons les bibliothèques Python suivantes :

pip3 install --upgrade django-dotenv beautifulsoup4 lxml boto3

Les variables

Nous lirons les variables spécifiques à AWS à partir d'un fichier ".env" (assurez-vous d'exclure ce fichier dans votre fichier .gitignore).

Comme je veux que le site web S3 soit un basculement pour mon vrai site web (il me suffit de rediriger l'enregistrement DNS pour le rendre "vivant"), la variable bucket_name est la même pour le site web original d'où je veux extraire les fichiers et pour le nom du bucket S3. Votre cas d'utilisation peut nécessiter un nom différent.

La variable extra_files est une liste contenant des fichiers supplémentaires que je souhaite sauvegarder sur S3 et qui peuvent ne pas être inclus dans votre fichier sitemap.xml.

dotenv.read_dotenv()
backup_region_name = os.environ.get("backup_region_name", "")
backup_aws_access_key_id = os.environ.get("backup_aws_access_key_id", "")
backup_aws_secret_access_key = os.environ.get("backup_aws_secret_access_key", "")
 
html_mime_type = 'text/html; charset=utf-8'
bucket_name = 'www.example.com'
extra_files = ['/', '/robots.txt', '/sitemap.xml', 'favicon.ico']
 
s3 = boto3.resource(
    's3',
    region_name=backup_region_name,
    aws_access_key_id=backup_aws_access_key_id,
    aws_secret_access_key=backup_aws_secret_access_key,
)

Obtenir une liste de pages web

Afin d'obtenir une liste de toutes les pages web à extraire du site original, j'ai décidé d'utiliser le fichier sitemap.xml du site, l'option la plus simple. Même s'il n'est pas complet à 100 %, c'est l'une des options les plus récentes.

La fonction get_sitemap a pour but de lire le fichier sitemap.xml du site web et d'énumérer tous les URI <loc>. Elle lira chaque page et appellera la fonction SaveFile pour enregistrer le contenu dans S3 si le code de retour est 200.

Puisque nous lisons la page web avec beautifulsoup, il va également analyser la page et ajouter tout fichier .css à la liste de fichiers extra_files, qui sera récupérée plus tard.

def get_sitemap(url):
    global extra_files
    full_url = f"https://{url}/sitemap.xml"
    with requests.Session() as req:
        r = req.get(full_url)
        soup = BeautifulSoup(r.content, 'lxml')
        links = [item.text for item in soup.select("loc")]
        for link in links:
            r = req.get(link)
            if r.status_code == 200:
                html_content = r.content
            else:
                print(f'\033[1;31;1m{link} {r.status_code}')
                continue
            soup = BeautifulSoup(r.content, 'html.parser')
            SaveFile(link, r.content, html_mime_type, soup.html["lang"])
 
            # Get all CSS links
            for css in soup.findAll("link", rel="stylesheet"):
                if css['href'] not in extra_files:
                    print('\033[1;37;1m', "Found the URL:", css['href'])
                    extra_files.append(css['href'])
    return

Enregistrement de pages web sur S3

Le but de la fonction SaveFile est de sauvegarder les pages web, les images, les fichiers .css ou tout autre fichier sur S3. J'ai choisi la classe "REDUCED_REDUNDANCY" pour S3 afin de réduire les coûts, à adapter selon vos besoins.

Comme j'ai un site multilingue, j'utilise le type de mime 'text/html ; charset=utf-8', et j'essaie également de lire la langue du fichier HTML afin de pouvoir la définir dans l'objet S3. Le script convertit également l'URL en noms 'utf-8' appropriés pour l'objet S3.

Mon site web n'utilise pas l'extension ".html" mais ajoute un "/" à la fin du nom de la page web. Dans S3, cela se traduit par un nom de "dossier" contenant le nom de la page web et un objet S3 nommé "/" contenant le contenu HTML.

J'ai nommé la page racine par défaut de mon site web "index.html" parce que c'est la page web par défaut configurée dans ma distribution CloudFront.

def SaveFile(file_name, file_content, mime_type, lang):
    global bucket_name
    my_url = urllib.parse.unquote(file_name, encoding='utf-8', errors='replace')
    my_path = urllib.parse.urlparse(my_url).path
    if my_path == '/':
        my_path = 'index.html'
    if my_path.startswith('/'):
        my_path = my_path[1:]
    print(f'\033[1;32;1m{file_name} -> {my_path} {lang}')
    bucket = s3.Bucket(bucket_name)
    if lang is not None:
        bucket.put_object(Key= my_path, Body=file_content, ContentType=mime_type, StorageClass='REDUCED_REDUNDANCY', CacheControl='max-age=0', ContentLanguage=lang)
    else:
        bucket.put_object(Key= my_path, Body=file_content, ContentType=mime_type, StorageClass='REDUCED_REDUNDANCY', CacheControl='max-age=0')

Enregistrement de fichiers supplémentaires

L'objectif de la fonction get_others est de récupérer les fichiers "supplémentaires", ceux qui ne sont pas inclus dans le fichier sitemap. Il peut s'agir de '/robots.txt', '/sitemap.xml', 'favicon.ico', etc. Nous utilisons la bibliothèque mimetypes pour deviner et définir la balise mime-types appropriée dans S3. Nous appelons la même fonction SaveFile pour sauvegarder sur S3.

def get_others(url):
    global extra_files
    with requests.Session() as req:
        for file in extra_files:
            my_url = requests.compat.urljoin(f"https://{url}", file)
 
            # Get MIME type using guess_type
            mime_type, encoding = mimetypes.guess_type(my_url)
            if mime_type is None:
                mime_type = html_mime_type
            print("\033[1;37;1mMIME Type:", mime_type)
 
            r = req.get(my_url)
            if r.status_code == 200:
                if mime_type == html_mime_type:
                    soup = BeautifulSoup(r.content, 'html.parser')
                    lang = soup.html["lang"]
                else:
                    lang = None
                SaveFile(my_url, r.content, mime_type, lang)
            else:
                print(f'\033[1;31;1m{my_url} {r.status_code}')
                continue
    return

Le code fini

Vous trouverez le code source complet de cet article à l'adresse suivante : https://github.com/Christophe-Gauge/python/blob/main/backup_website.py.

Ce script n'est peut-être pas capable de gérer tous vos cas d'utilisation, mais c'est, je l'espère, un bon début. N'hésitez pas à commenter ci-dessous et/ou à soumettre des demandes de modifications si vous souhaitez l'améliorer !

Commentaires publiés : 0

Tagged with:
AWS web