跳转到内容
Last updated

云手机文件上传与下载

本文给出完整的文件传输对接流程:将本地文件上传到云手机、从云手机导出文件、轮询异步任务,以及在超时后安全恢复。

选择 API

步骤开放 API本地 API
Base URLhttps://api.morelogin.comhttp://127.0.0.1:40000
认证Authorization: Bearer <token>启用本地 Token 时传入本地 Token
获取上传地址POST /cloudphone/uploadUrlPOST /api/cloudphone/upload/file/signedUrl
下发已上传文件POST /cloudphone/uploadFilePOST /api/cloudphone/upload/file
查询上传结果POST /cloudphone/uploadFileResultPOST /api/cloudphone/upload/file/result
创建文件导出任务POST /cloudphone/downloadPOST /api/cloudphone/download
查询导出结果POST /cloudphone/download/resultPOST /api/cloudphone/download/result

下文以开放 API 为例。本地 API 只需按上表替换 Base URL 和路径。云手机 ID 与任务 ID 必须始终按字符串保存;JavaScript 客户端不能先转换为 Number

本地 API multipart 快捷方式

当文件与 MoreLogin 客户端位于同一台电脑时,本地 API 还提供 POST /api/cloudphone/uploadFile,请求类型为 multipart/form-data

curl --request POST "http://127.0.0.1:40000/api/cloudphone/uploadFile" \
  --form 'id=1661515884160372' \
  --form 'uploadDest=/Download' \
  --form 'file=@./report.csv'

调用方位于远程服务器、文件较大,或需要通过 fileId 明确轮询状态时,建议使用下文的预签名 URL 流程。

上传流程

云手机临时存储MoreLogin API调用方云手机临时存储MoreLogin API调用方loop[直到终态]1. 获取预签名地址presignedUrl2. PUT 文件内容2xx3. 注册文件地址fileId, status=04. 查询上传结果null / status 0、1 或 2下发文件
云手机临时存储MoreLogin API调用方云手机临时存储MoreLogin API调用方loop[直到终态]1. 获取预签名地址presignedUrl2. PUT 文件内容2xx3. 注册文件地址fileId, status=04. 查询上传结果null / status 0、1 或 2下发文件

1. 获取预签名上传地址

curl --request POST "https://api.morelogin.com/cloudphone/uploadUrl" \
  --header "Authorization: Bearer $MORELOGIN_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "id": "1661515884160372",
    "fileName": "report.csv"
  }'

响应中的地址为 data.presignedUrl。临时对象会在 7 天后自动删除,但不会删除已经成功下发到云手机内的文件。

2. 上传文件内容

直接向预签名地址上传文件,不要在这个存储请求中携带 MoreLogin Bearer Token。

curl --location --request PUT \
  --upload-file "./report.csv" \
  "$PRESIGNED_URL"

仅在存储服务返回成功 HTTP 状态后继续。如果请求失败或超时,可以在地址仍有效时对同一个地址重试 PUT

3. 注册文件并下发到云手机

传入刚刚上传的对象地址。预签名 URL 的查询参数包含凭证;如果对象存储配置允许,请在保存或记录 URL 前移除查询参数。

FILE_URL="${PRESIGNED_URL%%\?*}"

curl --request POST "https://api.morelogin.com/cloudphone/uploadFile" \
  --header "Authorization: Bearer $MORELOGIN_TOKEN" \
  --header "Content-Type: application/json" \
  --data "$(jq -n \
    --arg id '1661515884160372' \
    --arg url "$FILE_URL" \
    --arg dest '/Download' \
    '{id: $id, url: $url, uploadDest: $dest, uploadType: 1}')"

普通文件使用 uploadType: 1。只有支持直播的 MP4 文件才使用 uploadType: 2。保存响应中的 data.fileId

4. 轮询上传结果

curl --request POST "https://api.morelogin.com/cloudphone/uploadFileResult" \
  --header "Authorization: Bearer $MORELOGIN_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "id": "1661515884160372",
    "fileId": "1661515884160111"
  }'
data / data.status含义处理方式
null任务暂时还不可见等待后继续轮询
0上传中等待后继续轮询
1上传成功停止轮询
2上传失败停止轮询并记录 requestId

建议每 2 秒查询一次,并根据文件大小设置客户端总超时。状态查询超时不代表上传任务失败,应继续查询已有 fileId,不要直接重复创建任务。

下载流程

这里的“下载”实际是先将云手机文件导出到临时地址,再由调用方下载,是一个两阶段异步流程。

临时存储云手机MoreLogin API调用方临时存储云手机MoreLogin API调用方loop[status=10 时继续轮询]1. 创建文件导出任务(id、filePath)读取并导出文件downId2. 查询导出结果(id、downId)status=10写入导出文件status=20、downUrl3. GET downUrl返回文件内容
临时存储云手机MoreLogin API调用方临时存储云手机MoreLogin API调用方loop[status=10 时继续轮询]1. 创建文件导出任务(id、filePath)读取并导出文件downId2. 查询导出结果(id、downId)status=10写入导出文件status=20、downUrl3. GET downUrl返回文件内容

如果轮询返回 30(失败)或 40(已取消),应停止流程,不要再请求 downUrl

1. 创建文件导出任务

必须传入包含文件名的 Android 绝对路径。

curl --request POST "https://api.morelogin.com/cloudphone/download" \
  --header "Authorization: Bearer $MORELOGIN_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "id": "1661515884160372",
    "filePath": "/sdcard/Download/report.csv"
  }'

保存 data.downId。该请求只创建导出任务,不会直接返回文件内容。

2. 轮询并获取下载地址

curl --request POST "https://api.morelogin.com/cloudphone/download/result" \
  --header "Authorization: Bearer $MORELOGIN_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "id": "1661515884160372",
    "downId": "1661515884160999"
  }'
data.status含义处理方式
10执行中等待后继续轮询
20成功使用 data.downUrl 下载
30失败停止并记录 requestId
40已取消停止;仍有需要时再创建新任务

状态为 20 后应尽快下载文件:

curl --location "$DOWN_URL" --output "./report.csv"

不要向 downUrl 携带 MoreLogin Bearer Token。预签名上传地址和下载地址都应视为敏感信息,不应写入应用日志。

完整 Python 示例

下面的代码需要安装 requests。它会同时检查 HTTP 状态和业务 code,设置轮询截止时间,并始终复用服务端返回的任务 ID。

import os
import time
from pathlib import Path

import requests

BASE_URL = "https://api.morelogin.com"
TOKEN = os.environ["MORELOGIN_TOKEN"]
PHONE_ID = "1661515884160372"
HEADERS = {"Authorization": f"Bearer {TOKEN}"}


def api_post(path, payload):
    response = requests.post(
        f"{BASE_URL}{path}", json=payload, headers=HEADERS, timeout=30
    )
    response.raise_for_status()
    body = response.json()
    if body.get("code") != 0:
        raise RuntimeError(
            f"API error code={body.get('code')} msg={body.get('msg')} "
            f"requestId={body.get('requestId')}"
        )
    return body


def wait_for(path, payload, terminal, timeout=300):
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        body = api_post(path, payload)
        data = body.get("data")
        if data is not None and data.get("status") in terminal:
            return body
        time.sleep(2)
    raise TimeoutError(f"Timed out while polling {path}; keep the task ID")


def upload_to_phone(local_path, destination="/Download"):
    path = Path(local_path)
    signed = api_post(
        "/cloudphone/uploadUrl", {"id": PHONE_ID, "fileName": path.name}
    )
    presigned_url = signed["data"]["presignedUrl"]
    with path.open("rb") as stream:
        put_response = requests.put(presigned_url, data=stream, timeout=120)
    put_response.raise_for_status()

    object_url = presigned_url.split("?", 1)[0]
    created = api_post(
        "/cloudphone/uploadFile",
        {
            "id": PHONE_ID,
            "url": object_url,
            "uploadDest": destination,
            "uploadType": 1,
        },
    )
    file_id = created["data"]["fileId"]
    result = wait_for(
        "/cloudphone/uploadFileResult",
        {"id": PHONE_ID, "fileId": file_id},
        terminal={1, 2},
    )
    if result["data"]["status"] != 1:
        raise RuntimeError(f"Upload failed; requestId={result.get('requestId')}")
    return file_id


def download_from_phone(remote_path, local_path):
    created = api_post(
        "/cloudphone/download", {"id": PHONE_ID, "filePath": remote_path}
    )
    down_id = created["data"]["downId"]
    result = wait_for(
        "/cloudphone/download/result",
        {"id": PHONE_ID, "downId": down_id},
        terminal={20, 30, 40},
    )
    if result["data"]["status"] != 20:
        raise RuntimeError(f"Download export failed; requestId={result.get('requestId')}")

    with requests.get(result["data"]["downUrl"], stream=True, timeout=120) as response:
        response.raise_for_status()
        with open(local_path, "wb") as output:
            for chunk in response.iter_content(1024 * 1024):
                if chunk:
                    output.write(chunk)
    return down_id


upload_to_phone("./report.csv")
download_from_phone("/sdcard/Download/report.csv", "./downloaded-report.csv")

生产接入检查清单

  • 必须检查 code == 0,HTTP 200 本身不代表业务操作成功。
  • idfileIddownId 始终按字符串保存。
  • 状态查询使用退避重试。创建任务请求超时后,如果已经获得任务 ID,应先查询原任务。
  • 限制并发传输数量,并遵守限流规则
  • 大文件传输前检查本地文件大小与云手机剩余存储空间。
  • 为排障记录 requestId、端点、云手机 ID、任务 ID 和终态,但必须脱敏 Token 与签名 URL。

完整字段定义参见云手机开放 API云手机本地 API;通用轮询与重试规则参见异步操作