跳转到主要内容

将静态文件从指定的源目录上传到目标目录或Amazon S3存储桶,在文件名中使用基于内容的哈希进行版本控制。

项目描述

简介

cdnupload通过在文件名中使用基于内容的哈希将您的网站静态文件上传到CDN,从而在避免版本控制问题的同时提供良好的缓存。cdnupload是

  • 快速且易于集成

  • 帮助您遵循网络最佳实践:使用CDN、良好的Cache-Control头、版本化文件名

  • 与任何语言编写的Web应用程序兼容

  • 使用Python编写(兼容Python 2和3)

此工具通过在各个资产文件名中包含基于内容的哈希,帮助您遵循性能最佳实践。

部署速度也非常快:只有实际发生更改的文件才会上传(带有新的哈希)。

cdnupload 安装简单

$ pip install cdnupload

使用简单

$ cdnupload /website/static s3://static-bucket --key-map=statics.json
uploading script.js to script_8f3283c6342816f7.js
uploading style.css to style_abcdef0123456789.css
writing key map JSON to statics.json

并且它 易于在大多数语言中集成,例如Python

import json, settings

def init_server():
    with open('statics.json') as f:
        settings.statics = json.load(f)

def static_url(path):
    return '//mycdn.com/' + settings.statics[path]

安装

cdnupload是一个Python包,在Python 3.4+和Python 2.7下运行。要从PyPI作为命令行脚本和全局Python环境安装它,只需输入

pip install cdnupload

如果您正在使用特定的Python版本或想将其安装在一个虚拟Python环境中,请先激活虚拟环境,然后运行pip install

此外,如果您将使用Amazon S3作为目标,则需要安装boto3包以与Amazon AWS交互。要安装boto3,请输入以下内容(如果您使用虚拟环境,请在虚拟环境中输入)

pip install boto3

安装cdnupload后,您只需键入cdnupload即可运行命令行脚本。或者,如果您需要针对特定的Python解释器运行它,可以使用python -m将脚本作为模块运行,如下所示

/path/to/my/python -m cdnupload

概述

cdnupload主要是一个命令行工具,用于将您网站上的静态文件上传到CDN(实际上是将文件上传到CDN的源服务器)。它还可以选择性地生成一个JSON“键映射”,将文件路径映射到目标键。目标键是包含基于文件内容的哈希的文件路径。这允许您设置CDN,以积极缓存您的静态文件,具有几乎无限的有效期时间(最大年龄)。

(有关CDN的简要介绍以及为什么您可能想使用它的原因,请参阅此文档的CDN部分。点击此处查看

上传静态文件时,您需要指定源目录和目标目录(或Amazon S3 URL或其他源伪URL)。例如,您可以将来自/website/static目录的所有静态文件上传到static-bucket,并使用以下命令将键映射输出到文件statics.json

cdnupload /website/static s3://static-bucket --key-map=statics.json

上传器将遍历源目录树,查询目标S3存储桶(或目录),并上传任何缺失的文件。例如,如果您有一个JavaScript文件和两个CSS文件,工具的输出可能如下所示

uploading script.js to script_0beec7b5ea3f0fdb.js
uploading style.css to style_62cdb7020ff920e5.css
uploading mobile.css to mobile_bbe960a25ea311d2.css
writing key map JSON to statics.json

如果您修改了mobile.css然后再次运行它,您会看到它只上传了更改过的文件

uploading mobile.css to mobile_6b369e490de120a9.css
writing key map JSON to statics.json

它不会自动删除目标目录中未使用的文件(因为目前部署的网站可能仍在使用它们)。要执行此操作,您需要使用删除操作

cdnupload /website/static s3://static-bucket --action=delete

以上上传完成后,输出可能如下所示

deleting mobile_bbe960a25ea311d2.css

有许多命令行选项可以控制要上传的文件,更改目标参数等。如果您需要高级功能或需要添加另一个目标“提供者”,可以直接使用Python API

您还需要将之与您的Web服务器集成,以便您的Web应用程序知道哈希映射,并可以输出正确的静态URL。这可以是一个简单的static_url模板函数,该函数使用键映射JSON将文件路径转换为目标键。有关详细信息,请参阅下面的Web服务器集成部分

为什么我应该使用CDN?

如果您不确定CDN是什么,或者您想知道为什么您应该使用它,本节就是为您准备的。

From Wikimedia under Creative Commons (NCDN_-_CDN.png)

CDN代表内容分发网络(Content Delivery Network),是一种服务,在世界各地的服务器上为您提供静态文件——这些服务器大量缓存,并且靠近您的用户。

因此,如果新泽西州的人请求https://mycdn.com/style.css,CDN几乎肯定会在东海岸或甚至是本地新泽西数据中心有一个缓存的版本,并将它以比您说出“HTTP/2”更快的速度提供给用户。

如果CDN没有缓存的文件版本,它将转而请求源服务器(文件托管的地方)。如果您使用类似Amazon S3的源服务器,该请求也将非常快速,用户仍然可以在较短时间内获得文件。从那时起,CDN将提供缓存的版本。

因为文件被大量缓存(理想情况下具有较长的有效期),您需要在文件名中包含版本号。cdnupload通过在文件名中附加基于文件内容的16位哈希来执行此操作。例如,style.css可能变成style_abcdef0123456789.css,然后在下一个版本中变为style_a0b1c2d3e4f56789.css

在一个我们运营的网站上,当我们开始使用Amazon Cloudfront CDN的cdnupload时,我们发现我们的静态文件加载时间从1500ms下降到220ms。

因此,如果你的网站流量很大,并且你需要从全球各地的位置获得良好的性能,你应该使用CDN。如果你有一个小型的个人网站,你可能不需要使用CDN。

使用Amazon CloudFront CDN与Amazon S3作为源服务器是一个很好的起点——和其他AWS产品一样,你只需为使用的字节付费,没有月费。

命令行使用

cdnupload命令行的格式是

cdnupload [options] source destination [dest_args]

其中options是短或长命令行选项(如-s--long)。如果你想的话,可以自由地将这些选项与位置参数混合。

source是你静态文件的源目录,例如/website/static。使用可选的--include--exclude参数以及下面描述的其他参数,来控制上传哪些文件。

目标地址和目标参数

destination是要上传到的目标目录,或用于上传到Amazon S3的s3://static-bucket/prefix路径。

你也可以指定目标地址的自定义方案(URL的schema://部分),并且cdnupload将尝试导入名为cdnupload_scheme的模块(该模块必须在PYTHONPATH上),并使用该模块的Destination类以及dest_args来创建目标实例。

例如,如果你为Google Cloud Storage创建了自己的上传器,你可能可以使用前缀gcs://并将模块命名为cdnupload_gcs。然后你可以使用gcs://my/path作为目标地址,并且cdnupload将使用cdnupload_gcs.Destination('gcs://bucket', **dest_args)来实例化目标实例。

有关自定义Destination子类的更多信息,请参阅自定义目标地址部分。

dest_args是传递给Destination类的关键字参数的目标特定参数(例如,对于s3://目标,有用的目标参数可能是max-age=86400region-name=us-west-2)。请注意,目标参数中的连字符将转换为下划线,因此region-name=us-west-2变为region_name='us-west-2'

要获取有关目标特定参数的帮助,请使用dest-help操作。例如,要显示S3特定的目标参数

cdnupload source s3:// --action=dest-help

常见参数

-h--help

显示有关这些命令行选项的帮助并退出。

-a ACTION--action ACTION

指定要执行的操作(默认为上传)

  • upload:从源上传文件到目标(但仅当它们尚未在目标上时)。

  • delete:删除目标处的未使用文件(源中不再存在的文件)。删除时要小心,并使用--dry-run先进行测试!

  • dest-help: 显示给定目标类的帮助信息和可用目标参数。

-d, --dry-run

显示脚本将上传或删除的内容,而不是实际执行。在运行--action=delete之前,此选项推荐使用,以确保不会删除超出预期的内容。

-e PATTERN, --exclude PATTERN

如果源文件的相对路径与给定的模式匹配(根据Python的fnmatch规则),则排除源文件。例如,使用*.txt排除所有文本文件,或使用__pycache__/*排除pycache目录下的所有内容。此选项可以多次指定以排除多个模式。

排除的优先级高于包含,因此您可以使用--include=*.txt,然后使用--exclude=docs/README.txt排除特定的文本文件。

-f, --force

在上传时,即使目标文件已存在,也强制上传所有文件(例如,在更新Amazon S3上的标题时很有用)。

在删除时,即使目标上的所有文件都将被删除,也允许删除操作发生(默认情况下,会阻止这种情况,以避免rm -rf风格的错误)。

-i PATTERN, --include PATTERN

如果指定,则仅包括相对路径与给定模式匹配的源文件(根据Python的fnmatch规则)。例如,使用*.png包括所有PNG图像,或使用images/*包括images目录下的所有内容。此选项可以多次指定以包括多个模式。

排除的优先级高于包含,因此您可以使用--include=*.txt,然后使用--exclude=docs/README.txt排除特定的文本文件。

-k FILENAME, --key-map FILENAME

将密钥映射写入给定的文件作为JSON(但仅在成功上传或删除后)。此文件可以由您的Web服务器使用,以生成静态文件的完整CDN URL。

JSON对象中的键是原始路径(相对于源根),对象中的值是目标路径(相对于目标根)。例如,JSON可能看起来像这样:{"script.js": "script_0beec7b5ea3f0fdb.js", ...}

-l LEVEL, --log-level LEVEL

设置日志输出的详细程度。级别必须是以下之一:

  • debug:最详细输出。记录脚本会跳过的文件。

  • verbose:详细输出。记录脚本开始、结束以及上传和删除发生(或如果进行--dry-run,将发生)时的情况。

  • default:默认日志输出级别。仅在脚本实际上传或删除文件时记录(无开始或结束日志)。如果没有要执行的操作,则不记录任何内容。

  • error:仅记录错误。

  • off:完全关闭所有日志记录。

-v, --version

显示cdnupload的版本号并退出。

较少使用的参数

--continue-on-errors

在发生上传或删除错误后继续。脚本仍将记录错误,并且如果有至少一个错误,它还将返回非零退出代码。默认情况下,在第一个错误时停止。

--dot-names

包括以点(.)开头的源文件和目录。默认情况下,将跳过以点开头的任何文件或目录。

--follow-symlinks

在遍历源树时跟随目录的符号链接。默认情况下,会跳过任何目录的符号链接。

--hash-length N

设置用于目标密钥的内容哈希的十六进制字符数。默认值为16。

--ignore-walk-errors

忽略遍历源树时的错误(例如,目录上的权限错误),除了列出源根目录时的错误。

Web服务器集成

除了使用命令行脚本来上传文件外,您还需要修改您的Web服务器,以便它知道如何生成包括文件名中基于内容的哈希在内的静态URL。

推荐的做法是使用由--key-map命令行参数写入的JSON键映射。您可以在服务器启动时将其加载到键值字典中,然后通过查找静态文件在字典中的相对路径来生成静态URL。

尽管JSON中的键是相对文件路径,但它们被规范化为始终使用/(正斜杠)作为目录分隔符,即使在Windows上也如此。这样,映射的消费者可以直接使用一致的路径分隔符查找文件。

以下是在您的Web服务器启动时加载键映射的简单示例(在启动时调用init_server()),然后定义一个函数来生成用于HTML模板的完整静态URL。此示例是用Python编写的,但您可以使用任何可以解析JSON并在映射中查找内容的语言。

import json
import settings

def init_server():
    settings.cdn_base_url = 'https://mycdn.com/'
    with open('statics.json') as f:
        settings.statics = json.load(f)

def static_url(rel_path):
    """Convert relative static path to full static URL (including hash)"""
    return settings.cdn_base_url + settings.statics[rel_path]

然后在您的HTML模板中,只需使用static_url函数(在此处作为Jinja2模板过滤器引用)引用静态文件。

<link rel="stylesheet" href="{{ 'style.css'|static_url }}">

如果您的Web服务器实际上是用Python编写的,您也可以直接导入cdnupload并使用与上传命令行相同的参数使用cdnupload.FileSource。这将在服务器启动时构建键映射,并且可能稍微简化部署过程。

import cdnupload
import settings

def init_server():
    settings.cdn_base_url = 'https://mycdn.com/'
    source = cdnupload.FileSource(settings.static_dir)
    settings.static_paths = source.build_key_map()

如果您有大量的静态文件,这不建议这样做,因为服务器启动时确实需要重新哈希所有文件。因此,对于较大的网站,最好在生产部署过程中生成键映射JSON并将其复制到您的应用程序服务器。

CSS中的静态URL

如果您在CSS中引用静态文件(例如,使用url(...)表达式的背景图像),您需要从CSS中删除它们并在HTML顶部生成一个内联的<style>部分,或者使用CSS上的后处理脚本来将URL从相对的改为全哈希URL。

对于小型网站,简单地从CSS中提取它们可能更简单。例如,对于这样的CSS规则

body.home {
    font-family: Verdana;
    font-size: 10px;
    background-image: url(/static/images/hero.jpg);
}

您只需删除background-image行,并将其放入相关页面的<head>部分的内联样式块中,如下所示

<head>
    <!-- other head elements; link to the stylesheet above -->
    <style type="text/css">
        body.home {
            background-image: url({{ 'images/hero.jpg'|static_url }});
        }
    </style>
</head>

但是,对于CSS引用了大量静态图像的大型网站,这很快就会变得难以管理。在这种情况下,您会想要使用像PostCSS这样的工具,通过键映射重写CSS中的静态URL为cdnupload URL。有一个名为postcss-url的PostCSS插件,您可以使用它使用自定义转换函数重写URL。

CSS重写应集成到您的构建或部署过程中,因为PostCSS规则需要访问上传器写出的JSON键映射。

Python API

cdnupload 是一个 Python 命令行脚本,但它也是一个 Python 模块,如果您需要对其进行定制或连接到高级功能,可以导入和扩展。它支持 Python 3.4+ 和 Python 2.7。

自定义目标位置

您可能需要扩展 cdnupload 的最常见原因是编写一个自定义的 Destination 子类(如果内置的文件或 Amazon S3 目标不适用于您)。

例如,如果您使用的是连接到名为“我的源”的源服务器的 CDN,您可能需要编写一个自定义子类以上传到您的源。您需要从 cdnupload.Destination 继承并实现初始化器以及 __str__walk_keysuploaddelete 方法。

import cdnupload
import myorigin

class Destination(cdnupload.Destination):
    def __init__(self, url, foo='FOO', bar=None):
        """Initialize destination instance with given "My Origin" URL
        (which should be in form my://server/path).
        """
        self.url = url
        self.conn = myorigin.Connection(url, foo=foo, bar=bar)

    def __str__(self):
        """Return a human-readable string for this destination."""
        return self.url

    def walk_keys(self):
        """Yield keys (files) that are currently on the destination."""
        for file in self.conn.get_files():
            yield file.name

    def upload(self, key, source, rel_path):
        """Upload a single file from source at rel_path to destination
        at given key. Normally this function will use the with statement
        "with source.open(rel_path)" to open the source file object.
        """
        with source.open(rel_path) as source_file:
            self.conn.upload_file_obj(source_file, key)

    def delete(self, key):
        """Delete a single file on destination at given key."""
        self.conn.delete_file(key)

要使用此自定义目标,将您的自定义代码保存到 cdnupload_my.py,并确保该文件位于您的 PYTHONPATH 上的某个位置。然后,如果您以以方案 my:// 开头的目标运行 cdnupload 命令行工具,它将自动导入 cdnupload_my 并查找一个名为 Destination 的类,将 my://server/path URL 和任何其他目标参数传递给初始化器。

请注意,当命令行工具将额外的 dest_args 传递给自定义目标时,它始终将它们作为字符串(如果指定的目标参数重复一次以上,则为字符串列表)传递。因此,如果您需要整数或其他类型,您需要在 __init__ 方法中进行转换。

上传和删除

顶级函数 upload()delete() 推动着 cdnupload。如果您想连接到 cdnupload 的 Python API,可以创建自己的命令行入口点。例如,您可以这样创建一个 myupload.py 脚本:

import cdnupload
import hashlib

source = cdnupload.FileSource('/path/to/my/statics',
                              hash_class=hashlib.md5)
destination = cdnupload.S3Destination('s3://bucket/path')
cdnupload.upload(source, destination)

在这里,我们对 FileSource 的哈希行为进行了轻微的自定义(将其从 SHA-1 更改为 MD5),然后执行上传。

upload() 函数将文件从源上传到目标,但只有当它们在目标中不存在(根据 destination.walk_keys)时才会这样做。

delete() 函数删除目标中的文件,如果它们在源中不再存在(根据 source.build_key_map)。

这两个函数都接受相同的参数集

  • source:源对象;可以是 FileSource 实例(或实现相同接口的对象),或字符串,在这种情况下,它通过 FileSource(source) 转换为源

  • destination:目标对象;可以是具体 Destination 子类的实例,或字符串,在这种情况下,它通过 FileDestination(destination) 转换为目标

  • force=False:如果为 True,则与指定 --force 命令行选项相同

  • dry_run=False:如果为 True,则与指定 --dry-run 命令行选项相同

  • continue_on_errors=False:如果为 True,则与指定 --continue-on-errors 命令行选项相同

这两个函数都返回一个 Result 命名元组,它有以下属性

  • source_key_map:源路径到目标键的映射,与 source.build_key_map() 返回的相同字典

  • destination_keys:包含目标键的集合,由destination.walk_keys()返回

  • num_scanned:扫描的总文件数(上传时的源文件,或删除时的目标键)

  • num_processed:处理的文件数(实际上已上传或删除的文件)

  • num_errors:错误数(当continue_on_errors为真时很有用)

自定义源

您还可以自定义文件的源。目前只有一个源类,即FileSource,它从文件系统中读取文件并生成文件哈希。您可以通过传递给FileSource初始化器的选项来控制它包含或排除哪些文件,以及如何对它们的 内容进行哈希以生成基于内容的哈希。

参数dot_namesincludeexcludeignore_walk_errorsfollow_symlinkshash_length对应于命令行选项--dot-names--include--exclude--ignore-walk-errors--follow-symlinks--hash-length

此外,您还可以使用参数hash_chunk_sizehash_class进一步自定义FileSource。在哈希时,文件以hash_chunk_size-字节块读取,hash_class实例化以生成哈希(必须具有hashlib样式签名)。

或者,如果您想自定义高级行为,可以子类化FileSource。例如,您可以覆盖FileSource.hash_file()对文本文件和二进制文件的处理,使所有文件都被视为二进制文件

from cdnupload import FileSource

class BinarySource(FileSource):
    def hash_file(self, rel_path):
        return FileSource.hash_file(self, rel_path, is_text=False)

要使用子类化的FileSource,您需要从Python直接调用实例的upload()delete()函数。目前无法通过cdnupload命令行脚本使用子类化的源。

日志记录

cdnupload函数使用标准的Python日志记录来记录所有操作。记录器名称为cdnupload,您可以使用Python日志记录函数来控制日志输出格式和详细程度(日志级别)。

例如,要记录所有错误,但只为cdnupload日志打开调试级别记录,可以这样做

import logging

logging.basicConfig(level=logging.ERROR)
logging.getLogger('cdnupload').setLevel(logging.DEBUG)

贡献

如果您在cdnupload中发现错误,请提供以下信息来打开一个问题

  • 完整的错误消息或跟踪回溯

  • cdnupload版本、Python版本以及操作系统类型和版本

  • 重现问题的步骤或测试用例(理想情况下)

如果您有功能请求、文档修复或其他建议,请打开一个问题,我们将讨论!

有关详细信息,请参阅cdnupload源树中的CONTRIBUTING.md

许可证

cdnupload根据MIT许可协议授权:有关详细信息,请参阅LICENSE.txt

请注意,在2017年8月之前,它根据AGPL加商业许可证的组合授权,但现在它是完全免费的。

关于作者

cdnupload由Ben Hoyt编写和维护:一位软件开发者、Python贡献者以及全能的软件极客。有关更多信息,请参阅他的个人网站benhoyt.com

项目详细信息


下载文件

下载适用于您平台的文件。如果您不确定选择哪个,请了解有关安装包的更多信息。

源代码发行版

cdnupload-1.0.4.tar.gz (33.8 kB 查看哈希值)

上传时间 源代码

支持