Copia de seguridad de un sitio web en S3

Clonar un sitio web en ejecución en S3 y CloudFront.

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

El propósito de este script Python es respaldar o migrar todas las páginas de un sitio web en ejecución a un bucket S3, para ser servido por una Distribución CloudFront.

Mi caso de uso es simple, quiero tener una copia de seguridad barata de mi sitio web en caso de que el servidor web tenga algún problema. Una de las maneras más baratas de ejecutar un sitio web hoy en día es tener un cubo de S3 habilitado para la web. No sólo es muy barato, también es extremadamente escalable.

Cuando digo "cubo S3 habilitado para web", claro, la gente ya no hace eso (por favor, no lo hagáis), ponle delante una distribución de CloudFront (también superescalable y barata).

Para empezar esa parte, puedes usar mi plantilla de CloudFormation, ver CloudFront and S3 Bucket CloudFormation Stack.

El Código

Las bibliotecas

Para ello, utilizaremos las siguientes bibliotecas de Python:

pip3 install --upgrade django-dotenv beautifulsoup4 lxml boto3

Las variables

Leeremos las variables específicas de AWS desde un archivo ".env" (asegúrate de excluirlo en tu archivo .gitignore).

Dado que quiero que el sitio web de S3 sea una conmutación por error para mi sitio web real (sólo requiere que vuelva a apuntar el registro DNS para que sea "en vivo"), la variable bucket_name es la misma para el sitio web original del que quiero extraer los archivos y para el nombre del bucket de S3. Su caso de uso puede requerir un nombre diferente.

La variable extra_files es una lista que contiene archivos extra que quiero respaldar en S3 y que pueden no estar incluidos en tu archivo 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,
)

Obtener una lista de páginas web

Para obtener una lista de todas las páginas web que extraer del sitio web original, decidí utilizar el archivo sitemap.xml del sitio, la opción más sencilla. Aunque puede que no esté completo al 100%, debería ser una de las opciones más actualizadas.

El propósito de la función get_sitemap es leer el archivo sitemap.xml del sitio web y enumerar a través de todos los <loc> URIs. Leerá cada página y llamará a la función SaveFile para guardar el contenido en S3 si el código de retorno es 200.

Como estamos leyendo la página web con beautifulsoup, también analizará la página y añadirá cualquier archivo .css a la lista de archivos extra_files, para recuperarlo más tarde.

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

Guardar páginas web en S3

El propósito de la función SaveFile es guardar las páginas web, imágenes, .css, o cualquier otro archivo a S3. Elegí la clase "REDUCED_REDUNDANCY" para S3 para reducir el costo, ajustar a sus necesidades.

Como tengo un sitio multilingüe, utilizo el tipo mime 'text/html; charset=utf-8', y también intento leer el idioma del archivo HTML para poder establecerlo en el objeto S3. El script también convierte la URL a nombres 'utf-8' adecuados para el objeto S3.

Mi sitio web no utiliza la extensión ".html", sino que añade un "/" al final del nombre de la página web. En S3, esto se convierte en un nombre de "carpeta" con el nombre de la página web y un objeto S3 llamado "/" que contiene el contenido HTML.

Nombré la página raíz predeterminada de mi sitio web 'index.html' porque es la página web predeterminada configurada en mi distribución de 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')

Guardar archivos adicionales

El propósito de la función get_others es recuperar los archivos "extra", los que no están incluidos en el archivo sitemap. Estos pueden incluir '/robots.txt', '/sitemap.xml', 'favicon.ico', y más. Utilizamos la biblioteca mimetypes para adivinar y establecer la etiqueta mime-types adecuada en S3. Llamamos a la misma función SaveFile para guardar en 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

El código terminado

Puede encontrar el código fuente completo de este artículo en https://github.com/Christophe-Gauge/python/blob/main/backup_website.py.

Este script puede no ser capaz de manejar todos sus casos de uso, pero es de esperar un buen comienzo. Por favor, ¡comenta a continuación y/o envía pull requests si lo mejoras!

Comentarios publicados: 0

Tagged with:
AWS web