无服务器应用程序,可单击一键将公共域Gutenberg.org电子书发送到您的Kindle!
#aws #python #serverless #书

我编写了一个无服务器的Web应用程序,该应用程序执行以下操作:

  1. 从Gutenberg.org下载页面(Gutenberg项目是一个非营利项目,旨在向公众提供免费的公共领域电子书)
  2. 如果URL是书籍下载页面,请在“下载此电子书表”中添加一列,其中包含链接和一个电子邮件的输入
  3. 用户将Kindle电子邮件放在输入字段中并单击“发送到Kindle”链接后,该电子书将发送到用户的Kindle!

这是一个超级有趣的项目,我将从开始到结束。它使用AWS API网关,AWS Lambda和AWS SES。在我们开始研究如何构建它的详细信息之前,您可以通过将“ joe@compellingsciencefiction.com”添加到您的Kindle Safe发送者列表中,然后将任何gutenberg.org添加到https://sendtokindle.compellingsciencefiction.com中:

>

https://sendtokindle.compellingsciencefiction.com/ebooks/67368

我的小应用程序(位于我的科幻网站的子域中托管)会将额外的“发送到Kindle”列添加到Gutenberg表中,您只需要将Kindle电子邮件地址放入小框中,然后单击发送链接。

无服务器体系结构

Architecture of the send to kindle application

该应用程序的体系结构很简单,只是调用lambda的API网关。 Lambda保存了一些有关下载书的元数据,然后在S3中下载了从Gutenberg下载所请求的电子书,并将其发送到指定的电子邮件地址。这是生成AWS资源的CDK代码:

from aws_cdk import (
aws_lambda as lambda_,
aws_apigateway as apigw,
aws_iam as iam,
aws_ecr as ecr,
aws_certificatemanager as acm,
aws_route53 as route53,
Duration,
Stack)
from constructs import Construct
import os

class SendToKindleStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # execution role
        lambda_role = iam.Role(self, id="sendtokindle-lambda",
            role_name='SendtokindleManagementRole',
            assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"),
            managed_policies= [
                        iam.ManagedPolicy.from_aws_managed_policy_name("service-role/AWSLambdaVPCAccessExecutionRole"),
                        iam.ManagedPolicy.from_aws_managed_policy_name("service-role/AWSLambdaBasicExecutionRole"),
                        iam.ManagedPolicy.from_aws_managed_policy_name("AmazonS3FullAccess"),
                        iam.ManagedPolicy.from_aws_managed_policy_name("AmazonSESFullAccess"),
                    ]
        )

        repo = ecr.Repository.from_repository_name(self, "SendToKindleRepo", "sendtokindlerepo")

        sendtokindle_management_lambda = lambda_.DockerImageFunction(self,
            "CSFsendtokindleManagementLambda",
            code=lambda_.DockerImageCode.from_ecr(
                repository=repo,
                tag=os.environ["CDK_DOCKER_TAG"]
                ),
            role=lambda_role,
            timeout=Duration.seconds(30)
        )

        api = apigw.LambdaRestApi(self,
            "csf-sendtokindle-management-endpoint",
            handler=sendtokindle_management_lambda,
            default_cors_preflight_options=apigw.CorsOptions(allow_origins=["*"])
        )

        custom_domain = apigw.DomainName(
            self,
            "custom-domain",
            domain_name="sendtokindle.compellingsciencefiction.com",
            certificate=acm.Certificate.from_certificate_arn(self,'cert',<cert arn str>),
            endpoint_type=apigw.EndpointType.EDGE
        )

        apigw.BasePathMapping(
            self,
            "base-path-mapping",
            domain_name=custom_domain,
            rest_api=api
        )

        hosted_zone = route53.HostedZone.from_hosted_zone_attributes(
            self,
            "hosted-zone",
            hosted_zone_id=<zone id str>,
            zone_name="compellingsciencefiction.com"
        )

        route53.CnameRecord(
            self,
            "cname",
            zone=hosted_zone,
            record_name="sendtokindle",
    domain_name=custom_domain.domain_name_alias_domain_name
        )

我已经将占位符置于此类领域以寻求隐私,但这是我部署的CDK代码。

您可以看到,此IAC的最长部分是DNS代码!如果您不在乎自定义域而只想使用API​​网关提供的域,那么您甚至不需要任何东西就要经过网关规范。

Lambda Python代码

这个lambda拔起双重 - 它都返回gutenberg.org html(带有修改的表),如果发送请求是下载epub映像,它还将电子书发送给kindle。可以做出许多改进,但是这种快速而肮脏的Lambda非常适合我的需求。

我想指出有关此代码的一些有趣的事情:

  1. 它将电子书下载到内存字节对象而不是文件中,然后使用内存文件构建MIME附件。
  2. 该代码对呼叫的区分如何不太强大 - 有很多破坏此lambda的方法,在这种情况下,它将返回通用的502服务器错误。
  3. 该代码使用AWS SES通过BOTO3发送电子邮件。以这种方式使用SES时要小心,有人绝对可以尝试通过垃圾邮件SES发送在我的AWS帐户上花费大量钱。您可以在SES(和API网关)上设置限制以减轻这样的攻击。<​​/li>
  4. 我使用BeautifulSoup来浏览Gutenberg.org HTML将我的列添加到下载表中。我不得不自省gutenberg.org html以在代码中找到正确的位置以插入我的列。
from bs4 import BeautifulSoup
import requests
import uuid
import json
import boto3
import time
from email import encoders
from email.mime.base import MIMEBase
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from io import BytesIO
from urllib.request import urlopen
import urllib

BUCKET = "S3 bucket name str"

def response(code, body):
    return {
            'statusCode': code,
            'headers': {
                'Access-Control-Allow-Headers': 'Content-Type',
                'Access-Control-Allow-Origin': '*',
                'Access-Control-Allow-Methods': 'OPTIONS,POST,GET',
                'Content-Type': 'application/json',
            },
            'body': body
        }


def send_ebook(url, filename, email):
    epubfile = BytesIO()
    print(url)
    try:
        with urlopen(url, timeout=10) as connection:
            epubfile.write(connection.read())
    except urllib.error.HTTPError as e:
        print(e.read().decode())
    from_email = "joe@compellingsciencefiction.com"
    to_email = email
    msg = MIMEMultipart()
    msg["Subject"] = "gutenberg ebook!"
    msg["From"] = from_email
    msg["To"] = to_email

    # Set message body
    body = MIMEText("your book!", "plain")
    msg.attach(body)

    epubfile.seek(0)
    part = MIMEApplication(epubfile.read())
    part.add_header("Content-Disposition",
                    "attachment",
                    filename=filename)
    msg.attach(part)

    # Convert message to string and send
    ses_client = boto3.client("ses", region_name="us-west-2")
    response = ses_client.send_raw_email(
        Source=from_email,
        Destinations=[to_email],
        RawMessage={"Data": msg.as_string()}
    )
    print(response)


def handler(event, context):
    print(event)
    try:
        print(event['path'])
    except:
        pass

    if "epub3.image" in event['path']:
        # this is a request to send an ebook
        path = event['path']
        email = event['queryStringParameters']['email']
        filename = path.replace("/ebooks/","").replace("3.images","")
        send_ebook(f"https://www.gutenberg.org/ebooks{path}", filename, email)
        client = boto3.client('s3')
        upload_id = uuid.uuid4().hex
        payload = {
            "timestamp": time.time(),
            "book_url": path,
            "user_email": email
        }
        client.put_object(Bucket=BUCKET,Key=f'{upload_id}.json', Body=json.dumps(payload).encode('utf-8'))
        return response(200, '{"status":"Sent '+path+' to '+email+'!"}')
    else:
        # return the gutenberg html with added column
        r = requests.get(f"https://www.gutenberg.org/{event['path']}")
        print(r.status_code)
        soup = BeautifulSoup(r.text.replace('"/','"https://www.gutenberg.org/').replace("https://www.gutenberg.org/ebooks","https://sendtokindle.compellingsciencefiction.com/ebooks"),features="html.parser")

        trs = soup.find_all("tr")
        for tr in trs:
            about = tr.get('about')
            if about and 'epub3' in about:
                print(tr)
                epubpath = f'{about.split("ebooks")[1]}'
                soup.append(BeautifulSoup(f"""
                <script>
                function buildlink() {{
                  let text = document.getElementById("kindleemail").value;
                  document.getElementById("csflink").href = "{epubpath}?email=" + text;
                }}
                </script>
                """))
                tr.append(BeautifulSoup(f"<td><a id='csflink' href='/{epubpath}'>Send<br>to<br>kindle<br>email:</a><br><input type='text' id='kindleemail' oninput='buildlink()'></td>", "html.parser"))
            else:
                tr.append(BeautifulSoup("<td class='noprint'></td>", "html.parser"))
        return {
            'statusCode': 200,
            'headers': {
                'Access-Control-Allow-Headers': 'Content-Type',
                'Access-Control-Allow-Origin': '*',
                'Access-Control-Allow-Methods': 'OPTIONS,POST,GET',
                'Content-Type': 'text/html;charset=utf-8',
            },
            'body': soup.prettify()
        }

如果其中任何一个都激发了您创建自己的小实用Web应用程序,请告诉我(您有我的compellingsciencefiction.com电子邮件地址:)。我喜欢听到这样的项目。