Hub Python Library documentation

将文件上传到 Hub

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v2.4.0).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

将文件上传到 Hub

共享您的文件和工作成果是 Hub 的重要组成部分。huggingface_hub 提供了多种将文件上传到 Hub 的选项。您可以独立使用这些函数,也可以将它们集成到您的库中,从而让用户与 Hub 的交互更加便捷。

每当您需要将文件上传到 Hub 时,都需要登录您的 Hugging Face 账户。有关身份验证的更多详细信息,请参阅此部分。

上传文件

使用 create_repo() 创建仓库后,您可以使用 upload_file() 将文件上传到该仓库。

指定要上传的文件路径、在仓库中上传文件的目标位置,以及要添加该文件的仓库名称。根据仓库类型,您可以选择将仓库类型设置为 dataset、model 或 space。

>>> from huggingface_hub import HfApi
>>> api = HfApi()
>>> api.upload_file(
...     path_or_fileobj="/path/to/local/folder/README.md",
...     path_in_repo="README.md",
...     repo_id="username/test-dataset",
...     repo_type="dataset",
... )

上传文件夹

使用 upload_folder() 函数将本地文件夹上传到现有仓库。指定要上传的本地文件夹路径、在仓库中上传文件夹的目标位置,以及要添加该文件夹的仓库名称。根据仓库类型,您可以选择将仓库类型设置为 dataset、model 或 space。

>>> from huggingface_hub import HfApi
>>> api = HfApi()

# Upload all the content from the local folder to your remote Space.
# By default, files are uploaded at the root of the repo
>>> api.upload_folder(
...     folder_path="/path/to/local/space",
...     repo_id="username/my-cool-space",
...     repo_type="space",
... )

默认情况下,.gitignore 文件会被用于确定哪些文件应提交。默认情况下,我们会检查提交中是否存在 .gitignore 文件;如果不存在,则检查 Hub 上是否存在该文件。请注意,只有位于目录根部的 .gitignore 文件才会被使用。我们不会检查子目录中的 .gitignore 文件。

如果不想使用硬编码的 .gitignore 文件,可以使用 allow_patterns 和 ignore_patterns 参数来筛选要上传的文件。这些参数接受单个模式或模式列表。模式是标准通配符(globbing 模式),文档见 此处。如果同时提供了 allow_patterns 和 ignore_patterns,则两个约束条件都会生效。

除了 .gitignore 文件和允许/忽略模式外,任何子目录中存在的 .git/ 文件夹都会被忽略。

>>> api.upload_folder(
...     folder_path="/path/to/local/folder",
...     path_in_repo="my-dataset/train", # Upload to a specific folder
...     repo_id="username/test-dataset",
...     repo_type="dataset",
...     ignore_patterns="**/logs/*.txt", # Ignore all text logs
... )

你还可以使用 delete_patterns 参数来指定要在同一提交中从仓库删除的文件。 如果你想在推送文件之前清理远程文件夹,但又不清楚其中已存在哪些文件,这会非常有用。

以下示例将本地 ./logs 文件夹上传到远程 /experiment/logs/ 文件夹。仅上传 txt 文件,但在此之前,会先删除仓库中所有之前的日志。所有这些操作都在一次提交中完成。

>>> api.upload_folder(
...     folder_path="/path/to/local/folder/logs",
...     repo_id="username/trained-model",
...     path_in_repo="experiment/logs/",
...     allow_patterns="*.txt", # Upload all local text files
...     delete_patterns="*.txt", # Delete all remote text files before
... )

文件如何上传

当 hf_xet 已安装(默认情况下即如此)时,upload_folder() 会通过流式管道上传文件:文件会与 Hub 进行比对,上传至 Xet 存储后端(该后端内部会执行分块、去重和传输重试),并以自适应批次进行提交,所有步骤均并行执行。实际上这意味着:

  • 任意大小的文件夹:较小的文件夹会在单次提交中上传,而包含大量文件的文件夹会自动拆分为多个提交,以确保不超过服务器限制。当发生这种情况时,后续提交的提交信息会带有 (part 2)、(part 3)、… 这样的后缀。
  • 可恢复:如果上传因任何原因中断,只需重新运行相同的调用即可。已提交的文件会被检测并跳过,已上传的数据块会被去重——重新上传它们几乎不会传输任何数据。整个过程不涉及本地状态:您甚至可以从另一台机器恢复上传。一个例外是:当使用 create_pr=True 时,重新运行会打开一个新的拉取请求。我们建议在恢复上传时,改为使用 revision="refs/pr/N" 重新运行。
  • 无重复读取:文件在分块上传时即进行哈希计算,仅需一次读取。上传开始前没有单独的“哈希”阶段。

实时进度显示会跟踪以下三个阶段:

Found 5,000 files to upload
  Preparing   ████████████████████  5,000 / 5,000 ✓
  Uploading   ██████████████░░░░░░  423 / 603 files  3.8GB · 19.7MB/s
  Committing  ██████████████████░░  4,580 / 5,000  6 commits

如果未安装 hf_xet,upload_folder() 将回退到旧版行为:先对所有内容进行哈希计算,通过 HTTP 上传,然后创建单个提交。我们始终建议保持 hf_xet 处于已安装状态,以获得更好的健壮性

从 CLI 上传

您可以在终端中使用 hf upload 命令将文件直接上传到 Hub。该命令内部使用了上述的 upload_file() 和 upload_folder() 辅助函数。

您可以上传单个文件或整个文件夹:

# Usage:  hf upload [repo_id] [local_path] [path_in_repo]
>>> hf upload Wauplin/my-cool-model ./models/model.safetensors model.safetensors
https://huggingface.co/Wauplin/my-cool-model/blob/main/model.safetensors

>>> hf upload Wauplin/my-cool-model ./models .
https://huggingface.co/Wauplin/my-cool-model/tree/main

local_path 和 path_in_repo 是可选的,可以隐式推断。如果未设置 local_path,该工具会检查是否存在与 repo_id 同名的本地文件夹或文件。如果存在,其内容将被上传。否则,将抛出异常,要求用户显式设置 local_path。无论如何,如果未设置 path_in_repo,文件将上传到仓库的根目录。

目标也可以表示为单个 hf:// URI,其语法为 hf://[<TYPE>/]<ID>[@<REVISION>][/<PATH>](完整语法请参阅 HF URIs 参考)。仓库类型、修订版本和 path_in_repo 将从 URI 中读取,因此不能与 --repo-type 和 --revision 选项同时使用:

# Upload a single file to a dataset on a specific branch
>>> hf upload hf://datasets/Wauplin/my-cool-dataset@my-branch/data/train.csv ./train.csv
https://huggingface.co/datasets/Wauplin/my-cool-dataset/blob/my-branch/data/train.csv

为了在上传大文件时获得最大吞吐量,请设置 HF_XET_HIGH_PERFORMANCE=1 环境变量。这将启用 hf_xet 的高性能模式,该模式会充分利用可用的带宽和 CPU 核心。注意:由于 hf_transfer 已被移除并替换为 hf_xet,因此不再使用旧版的 HF_HUB_ENABLE_HF_TRANSFER=1 标志——请改为设置 HF_XET_HIGH_PERFORMANCE=1。

有关 CLI 上传命令的更多详细信息,请参阅 CLI 指南。

上传大文件夹

upload_folder() 和 hf upload 命令是将文件上传到 Hub 的常用解决方案,即使是非常大的文件夹也适用。文件会通过多次提交流式传输到 Hub,如果过程中断,流程会自动恢复。只需重新运行相同的调用,已上传的文件会被跳过。

>>> api.upload_folder(
...     repo_id="HuggingFaceM4/Docmatix",
...     repo_type="dataset",
...     folder_path="/path/to/local/docmatix",
... )

或从终端:

hf upload HuggingFaceM4/Docmatix --repo-type=dataset /path/to/local/docmatix

大型上传的技巧与窍门

在处理仓库中的大量数据时,有一些限制需要注意。考虑到流式传输数据所需的时间,如果在流程末尾上传/推送失败,或者在 hf.co 上或本地操作时遇到体验下降的情况,都会非常令人烦恼。

请参阅我们的仓库限制与建议指南,了解如何在 Hub 上组织仓库的最佳实践。接下来,让我们分享一些实用技巧,让您的上传过程尽可能顺畅。

  • 从小处着手:我们建议先用少量数据来测试你的上传脚本。当失败只花费很少时间时,迭代脚本会更容易。
  • 预期会出现失败:流式传输大量数据是一项具有挑战性的任务。你无法预知会发生什么,但最好始终假设至少会有一次失败——无论原因是你的机器、你的网络连接还是我们的服务器。例如,如果你计划上传大量文件,最好在上传下一批文件之前,在本地记录哪些文件已经上传过。可以确保已提交的 LFS 文件永远不会被重复上传,但在客户端进行检查仍然可以节省一些时间。upload_folder() 正是为你完成这项工作的。
  • 使用 hf_xet:它利用了 Hub 的新存储后端,使用 Rust 编写,现已向所有人开放。实际上,在使用 huggingface_hub 时,hf_xet 已默认启用。为了获得最佳性能,请将 HF_XET_HIGH_PERFORMANCE=1 设置为环境变量。请注意,启用高性能模式后,该工具将尝试使用所有可用的带宽和 CPU 核心。

高级功能

在大多数情况下,你只需要使用 upload_file() 和 upload_folder() 即可将文件上传到 Hub。 不过,huggingface_hub 还提供了更多高级功能,让操作更加便捷。让我们来看看这些功能!

更快的上传

利用 hf_xet 实现更快的上传,它是 xet-core 库的 Python 绑定,通过基于块的重复数据删除来加速上传和下载。hf_xet 可与 huggingface_hub 无缝集成,但它使用的是 Rust xet-core 库和 Xet 存储,而非 LFS。

hf_xet 使用 Xet 存储系统,该系统将文件拆分为不可变的块,并将这些块的集合(称为块或 xorbs)存储在远程,在请求时检索它们以重新组装文件。在上传时,在确认用户有权写入此仓库后,hf_xet 会扫描文件,将其拆分为块,并将这些块收集到 xorbs 中(并在已知块之间进行去重),然后会上传这些 xorbs 到 Xet 内容寻址服务(CAS),该服务将验证 xorbs 的完整性,注册 xorb 元数据以及 LFS SHA256 哈希(以支持查找/下载),并将 xorbs 写入远程存储。

要启用此功能,只需安装最新版本的 huggingface_hub:

pip install -U "huggingface_hub"

从 huggingface_hub 0.32.0 版本开始,此操作还将安装 hf_xet。

所有其他 huggingface_hub API 将继续正常工作,无需任何修改。要了解 Xet 存储和 hf_xet 的优势,请参阅此 部分。

集群 / 分布式文件系统上传注意事项

从集群上传时,待上传的文件通常位于分布式或网络文件系统(如 NFS、EBS、Lustre、Fsx 等)上。Xet 存储会将这些文件分块,并在本地将其写入块(也称为 xorbs),待块写入完成后即进行上传。为了在从分布式文件系统上传时获得更好的性能,请确保将 HF_XET_CACHE 设置为位于本地磁盘(例如本地 NVMe 或 SSD 磁盘)上的目录。Xet 缓存的默认位置在 HF_HOME 下的(~/.cache/huggingface/xet),而该位置通常位于用户主目录中,往往也处于分布式文件系统上。

非阻塞上传

在某些情况下,你希望在不阻塞主线程的情况下推送数据。这在继续训练的同时上传日志和工件时尤为有用。为此,你可以在 upload_file() 和 upload_folder() 中使用 run_as_future 参数。这将返回一个 concurrent.futures.Future 对象,你可以用它来检查上传状态。

>>> from huggingface_hub import HfApi
>>> api = HfApi()
>>> future = api.upload_folder( # Upload in the background (non-blocking action)
...     repo_id="username/my-model",
...     folder_path="checkpoints-001",
...     run_as_future=True,
... )
>>> future
Future(...)
>>> future.done()
False
>>> future.result() # Wait for the upload to complete (blocking action)
...

使用 run_as_future=True 时,后台任务会被放入队列。这意味着可以确保任务按正确的顺序执行。

尽管后台任务主要用于上传数据/创建提交,但你可以使用 run_as_future() 将任意方法加入队列。例如,你可以使用它在后台创建仓库,然后向其上传数据。上传方法中内置的 run_as_future 参数只是它的一个别名。

>>> from huggingface_hub import HfApi
>>> api = HfApi()
>>> api.run_as_future(api.create_repo, "username/my-model", exists_ok=True)
Future(...)
>>> api.upload_file(
...     repo_id="username/my-model",
...     path_in_repo="file.txt",
...     path_or_fileobj=b"file content",
...     run_as_future=True,
... )
Future(...)

在仓库之间复制文件

使用 copy_files() 可在 Hub 上的不同仓库之间复制文件或文件夹,而无需下载或重新上传大型数据。当您需要在不同模型变体之间复制权重、在仓库之间复制数据集文件,或在您的多个仓库之间重新组织文件时,此功能非常有用。其底层机制是通过 CommitOperationCopy 操作创建一个提交。

>>> from huggingface_hub import HfApi
>>> api = HfApi()

# Copy a single file between repos
>>> api.copy_files(
...     "hf://username/source-model/weights.safetensors",
...     "hf://username/target-model/weights.safetensors",
... )

# Copy an entire folder
>>> api.copy_files(
...     "hf://datasets/username/source-dataset/data/",
...     "hf://datasets/username/target-dataset/data/",
... )

您也可以在同一个仓库内复制:

# Duplicate a file in the same repo
>>> api.copy_files(
...     "hf://username/my-model/config.json",
...     "hf://username/my-model/backup/config.json",
... )

在复制文件夹时,源路径末尾的 / 采用 rsync 风格的语义,表示复制该文件夹的内容,而不将文件夹本身嵌套其中。如果末尾没有 /,则文件夹本身会被嵌套到目标位置。

copy_files() 还支持将文件复制到 Buckets。有关更多详细信息,请参阅 Buckets 指南。

计划上传

Hugging Face Hub 让保存和版本化数据变得轻而易举。然而,当需要数千次更新同一个文件时,存在一些限制。例如,您可能希望保存训练过程的日志,或已部署 Space 的用户反馈。在这些情况下,将数据作为数据集上传到 Hub 是合理的,但正确执行起来可能颇具挑战。主要原因是,您不希望为数据的每次更新都创建版本,因为这会使 git 仓库变得无法使用。CommitScheduler 类为这一问题提供了解决方案。

其思路是运行一个后台任务,定期将本地文件夹推送到 Hub。假设你有一个 Gradio Space,它接收一些文本作为输入并生成两个翻译版本,然后用户可以选择他们偏好的翻译。对于每次运行,你希望保存输入、输出以及用户偏好,以便分析结果。这是 CommitScheduler 的一个完美用例:你需要将数据保存到 Hub(可能包含数百万条用户反馈),但你并不需要实时保存每个用户的输入。相反,你可以将数据保存在本地的 JSON 文件中,并每 10 分钟上传一次。例如:

>>> import json
>>> import uuid
>>> from pathlib import Path
>>> import gradio as gr
>>> from huggingface_hub import CommitScheduler

# Define the file where to save the data. Use UUID to make sure not to overwrite existing data from a previous run.
>>> feedback_file = Path("user_feedback/") / f"data_{uuid.uuid4()}.json"
>>> feedback_folder = feedback_file.parent

# Schedule regular uploads. Remote repo and local folder are created if they don't already exist.
>>> scheduler = CommitScheduler(
...     repo_id="report-translation-feedback",
...     repo_type="dataset",
...     folder_path=feedback_folder,
...     path_in_repo="data",
...     every=10,
... )

# Define the function that will be called when the user submits its feedback (to be called in Gradio)
>>> def save_feedback(input_text:str, output_1: str, output_2:str, user_choice: int) -> None:
...     """
...     Append input/outputs and user feedback to a JSON Lines file using a thread lock to avoid concurrent writes from different users.
...     """
...     with scheduler.lock:
...         with feedback_file.open("a") as f:
...             f.write(json.dumps({"input": input_text, "output_1": output_1, "output_2": output_2, "user_choice": user_choice}))
...             f.write("\n")

# Start Gradio
>>> with gr.Blocks() as demo:
>>>     ... # define Gradio demo + use `save_feedback`
>>> demo.launch()

就是这样!用户输入/输出和反馈将作为数据集在 Hub 上可用。通过使用唯一的 JSON 文件名,可以确保您不会覆盖之前运行产生的数据,也不会覆盖其他同时向同一仓库推送数据的 Spaces/副本。

有关 CommitScheduler 的更多详细信息,以下是您需要了解的内容:

  • 仅追加:假设您只会向该文件夹中添加内容。您只能向现有文件追加数据或创建新文件。删除或覆盖文件可能会导致您的仓库损坏。
  • git history: 调度器将每 every 分钟提交一次该文件夹。为避免过度污染 git 仓库,建议将最小值设置为 5 分钟。此外,调度器被设计为避免空提交。如果在文件夹中未检测到新内容,则计划中的提交将被丢弃。
  • errors: 调度器作为后台线程运行。在实例化该类时启动,并持续运行,直到调用 stop() 或进程退出。如果上传过程中发生错误(例如:连接问题),该错误会被记录,调度器将在下一次计划提交时重试。
  • scripts: 在生命周期有限的脚本(例如训练)中,将调度器用作上下文管理器。离开 with 块时会触发最后一次上传,等待其完成并停止调度器。
  • 线程安全:在大多数情况下,可以安全地假设您可以写入文件而无需担心锁文件。当调度器正在上传时,即使您向该文件夹写入内容,调度器也不会崩溃或损坏。实际上,对于高负载的应用程序,可能会发生并发问题。在这种情况下,我们建议使用 scheduler.lock 锁来确保线程安全。该锁仅在调度器扫描文件夹以检测更改时被阻塞,而在上传数据时不会阻塞。您可以放心地认为,这不会影响您 Space 上的用户体验。

Space 持久化演示

将 Space 中的数据持久化到 Hub 上的 Dataset 是 CommitScheduler 的主要用例。根据具体用例,您可能希望以不同的方式组织数据。该结构必须能够应对并发用户和重启,这通常意味着需要生成 UUID。除了健壮性之外,您还应以 🤗 Datasets 库可读的格式上传数据,以便后续复用。我们创建了一个 Space,演示如何保存多种不同的数据格式(您可能需要根据自身的具体需求对其进行调整)。

自定义上传

CommitScheduler 假设你的数据是仅追加的,并且应以“原样”上传。不过,你可能希望自定义数据上传的方式。你可以通过创建一个继承自 CommitScheduler 的类并重写 push_to_hub 方法来实现(你可以按任意方式重写它)。它保证会在后台线程中每隔 every 分钟被调用一次。你无需担心并发和错误处理,但必须注意其他方面,例如推送空提交或重复数据。

在下面的(简化)示例中,我们重写了 push_to_hub,将所有 PNG 文件打包成一个归档文件,以避免给 Hub 上的仓库带来过大负载:

class ZipScheduler(CommitScheduler):
    def push_to_hub(self):
        # 1. List PNG files
        png_files = list(self.folder_path.glob("*.png"))
        if len(png_files) == 0:
            return None  # return early if nothing to commit

        # 2. Zip png files in a single archive
        with tempfile.TemporaryDirectory() as tmpdir:
            archive_path = Path(tmpdir) / "train.zip"
            with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as zip:
                for png_file in png_files:
                    zip.write(filename=png_file, arcname=png_file.name)

            # 3. Upload archive
            self.api.upload_file(..., path_or_fileobj=archive_path)

        # 4. Delete local png files to avoid re-uploading them later
        for png_file in png_files:
            png_file.unlink()

重写 push_to_hub 时,您可以访问 CommitScheduler 的属性,尤其是:

  • HfApi 客户端:api
  • 文件夹参数:folder_path 和 path_in_repo
  • 仓库参数:repo_id、repo_type、revision
  • 线程锁:lock

如需查看自定义调度器的更多示例,请访问我们的演示 Space,其中包含针对不同使用场景的各种实现。

create_commit

upload_file() 和 upload_folder() 函数是高层 API,通常使用起来非常方便。如果您不需要在更低的层级进行操作,我们建议先尝试使用这些函数。但是,如果您希望在提交(commit)级别进行操作,可以直接使用 create_commit() 函数。

create_commit() 支持三种类型的操作:

  • CommitOperationAdd 用于将文件上传到 Hub。如果该文件已存在,则文件内容会被覆盖。此操作接受两个参数:

    • path_in_repo: 要上传文件的目标仓库路径。
    • path_or_fileobj:文件系统中的文件路径或类文件对象。这是要上传到 Hub 的文件内容。
  • CommitOperationDelete 用于从仓库中删除文件或文件夹。此操作接受 path_in_repo 作为参数。

  • CommitOperationCopy 用于在仓库内部或跨仓库复制文件。此操作接受以下参数:

    • src_path_in_repo:要复制的文件在仓库中的路径。
    • path_in_repo:文件应被复制到的仓库路径。
    • src_revision:可选 - 如果您想从其他分支/修订版本复制文件,则指定要复制的文件修订版本。
    • src_repo_id:可选 - 要从中复制的源仓库(例如 "username/source-model")。默认为目标仓库。
    • src_repo_type:可选 - 源仓库的类型("model"、"dataset" 或 "space")。当设置了 src_repo_id 时必填。

例如,如果您想在 Hub 仓库中上传两个文件并删除一个文件:

  1. 使用适当的 CommitOperation 来添加或删除文件,以及删除文件夹:
>>> from huggingface_hub import HfApi, CommitOperationAdd, CommitOperationDelete
>>> api = HfApi()
>>> operations = [
...     CommitOperationAdd(path_in_repo="LICENSE.md", path_or_fileobj="~/repo/LICENSE.md"),
...     CommitOperationAdd(path_in_repo="weights.h5", path_or_fileobj="~/repo/weights-final.h5"),
...     CommitOperationDelete(path_in_repo="old-weights.h5"),
...     CommitOperationDelete(path_in_repo="logs/"),
...     CommitOperationCopy(src_path_in_repo="image.png", path_in_repo="duplicate_image.png"),
... ]
  1. 将您的操作传递给 create_commit():
>>> api.create_commit(
...     repo_id="lysandre/test-model",
...     operations=operations,
...     commit_message="Upload my model weights and license",
... )

除了 upload_file() 和 upload_folder() 之外,以下函数在底层也使用了 create_commit():

如需更详细的信息,请参阅 HfApi 参考文档。

在提交之前预上传 LFS 文件

在某些情况下,你可能希望在调用 commit 之前将大文件上传到 S3 之前。例如,如果你正在提交一个由多个在内存中生成的分片组成的数据集,就需要逐个上传这些分片,以避免内存溢出问题。一种解决方案是将每个分片作为仓库上的单独 commit 进行上传。虽然这种做法完全有效,但其缺点是会生成数十个 commit,从而可能使 git 历史变得混乱。为了解决这个问题,你可以先将文件逐个上传到 S3,然后在最后创建一个单一的 commit。这可以通过将 preupload_lfs_files() 与 create_commit() 结合使用来实现。

这是一种面向高级用户的方法。在绝大多数情况下,直接使用 upload_file()、upload_folder() 或 create_commit() 来处理文件预上传的底层逻辑是最佳选择。preupload_lfs_files() 的主要注意事项是,在实际创建提交之前,上传的文件在 Hub 上的仓库中尚不可访问。如果您有任何疑问,欢迎在我们的 Discord 或 GitHub issue 中联系我们。

下面是一个简单的示例,说明如何预上传文件:

>>> from huggingface_hub import CommitOperationAdd, preupload_lfs_files, create_commit, create_repo

>>> repo_id = create_repo("test_preupload").repo_id

>>> operations = [] # List of all `CommitOperationAdd` objects that will be generated
>>> for i in range(5):
...     content = ... # generate binary content
...     addition = CommitOperationAdd(path_in_repo=f"shard_{i}_of_5.bin", path_or_fileobj=content)
...     preupload_lfs_files(repo_id, additions=[addition])
...     operations.append(addition)

>>> # Create commit
>>> create_commit(repo_id, operations=operations, commit_message="Commit all shards")

首先,我们逐个创建 CommitOperationAdd 对象。在实际场景中,这些对象将包含生成的分片。每个文件在生成下一个文件之前都会先上传。在 preupload_lfs_files() 步骤中,CommitOperationAdd 对象会被修改。你应该仅将其用于直接传递给 create_commit()。该对象的主要更新是其中移除了二进制内容,这意味着如果你没有保留对它的另一个引用,它将被垃圾回收。这是预期行为,因为我们不希望将已上传的内容保留在内存中。最后,我们将所有操作传递给 create_commit() 来创建提交。你可以传递尚未处理的额外操作(添加、删除或复制),它们将被正确处理。

Update on GitHub