Skip to main content

控制台 API

通过控制台 API 你可以与控制台的各项资源进行交互,例如创建文件系统、浏览文件列表,查看回收站和设置 ACL 等。

本文主要介绍请求签名和身份认证, 查看 API schema 文档 以了解控制台支持哪些 API 以及 API 的数据格式。

URL 和数据格式

控制台 API 遵循 RESTful API 风格,URL 是 https://juicefs.com/api/v1,使用 JSON 作为数据交换格式。以获取文件系统列表为例, 请求参数如下:

GET /api/v1/volumes

Host: juicefs.com
Authorization: 4dca13af31e45740c1c1fe3acaca8752a093b43ccc169f890f1305f51f038bf8
...

返回的数据为:

[
{
"id": 1,
"name": "test",
"region": 1,
"bucket": "http://test.s3.us-east-1.amazonaws.com",
"blockSize": 4096,
"compress": "lz4",
"compatible": false,
"access_rules": [
{
"iprange": "*",
"token": "ec4da70f494f58aac5eee391e6c5986f0b99945",
"readonly": false,
"appendonly": false
}
],
"owner": 5,
"size": 20480,
"inodes": 5,
"created": "2022-07-14T09:57:10.888914Z",
"extend": "",
"storage": null
}
]

使用指南

创建 API 密钥对

  • 打开控制台的账户设置页面
  • 点击 「添加新的 API 密钥对」 按钮
  • 在打开的对话框中填写「描述」和选择「过期时间」, 点击「确定」就创建好了 API 密钥对。

请求签名

在发送 API 请求之前, 还需要使用 API 密钥对对请求进行签名。

为了方便描述,假设创建的 API 密钥对为:

  • Access key: ac7418402ce0ce838ba87eb3a6be72af313cd7028e18007799c0d5651c326925
  • Secret key: 5f0c5a5d51515947788fa7b8244acebe166aedd9de28b26ef716888a613c3d92

另外,以创建文件系统的请求为例:

POST /api/v1/volumes?a=1&a=2&b=3&c=4

Host: juicefs.com
Content-Type: application/json
...


{"name": "test", "bucket": "https://test.s3.us-east-1.amazonaws.com"}
备注

上面的请求参数 a=1&a=2&b=3&c=4 实际上是不存在的, 这里加上是为了方便介绍签名算法。

签名算法如下:

  • 记录下当前的时间戳,假设等于 1663245320

    服务端会通过时间戳判断此次请求的发起时间,如果和服务器收到请求的时间相差超过 5 分钟, 服务器会抛弃该请求。

  • 按顺序处理请求头,字段名转为小写字符,然后用 : 连接字段名和值,最后用 \n 连接所有字段。目前参与计算的请求头只有 Host。

    得到的结果为 host:juicefs.com

  • 对查询参数进行排序和编码, 如果没有查询参数则使用空字符串。

    • 排序规则: 先按照字段名排序, 因为需要处理同一个字段对应多个值的情况, 所以还需要对字段的值进行排序
    • 编码规则: 对字段名和值进行 URL 编码, 然后用 = 连接字段名和值, 最后用 & 连接所有字段

    请求参数 a=1&a=2&b=3&c=4 中包含三个字段 a, b, c,其中字段 a 有两个值。先按字段名排序得到 a < b < c,对于字段 a 还需要对它的两个值进行排序,最终得到的结果为 a=1&a=2&b=3&c=4

  • 对请求体进行 SHA256 哈希, 如果没有请求体则使用空字符串。

    得到的结果为 a81f7bf3a5740146fe1eedc891f1f8f063dc428a88ac590147d1cf056bdad04b

  • \n 按顺序拼接时间戳、请求方法、请求路径、请求头、查询参数和请求体。

    得到的结果为:

    1663245320\n
    POST\n
    /api/v1/volumes\n
    host:juicefs.com\n
    a=1&a=2&b=3&c=4\n
    a81f7bf3a5740146fe1eedc891f1f8f063dc428a88ac590147d1cf056bdad04b
  • 使用 Secret key 对拼接后的字符串进行 HMAC-SHA256 签名。

    得到的结果为 3646d11235b08cd856278cb68bd5d2bc7aeec5c593590813e1da43a22d3a9835

身份认证

计算完签名之后,还需要使用 Access key、时间戳(请求签名中使用的时间戳)、签名和版本号生成一个 Token 用于身份认证。

版本号是可选的,目前只有一个版本,将其设置为 1 即可。

步骤如下:

  • 将这些字段组成一个 JSON 格式的数据, 得到的结果为:

    {
    "access_key": "ac7418402ce0ce838ba87eb3a6be72af313cd7028e18007799c0d5651c326925",
    "timestamp": 1663245320,
    "signature": "3646d11235b08cd856278cb68bd5d2bc7aeec5c593590813e1da43a22d3a9835",
    "version": 1
    }
  • 使用 base64 对上一步的 JSON 数据进行编码,得到结果为:

    ewogICJhY2Nlc3Nfa2V5IjogImFjNzQxODQwMmNlMGNlODM4YmE4N2ViM2E2YmU3MmFmMzEzY2Q3MDI4ZTE4MDA3Nzk5YzBkNTY1MWMzMjY5MjUiLAogICJ0aW1lc3RhbXAiOiAxNjYzMjQ1MzIwLAogICJzaWduYXR1cmUiOiAiMzY0NmQxMTIzNWIwOGNkODU2Mjc4Y2I2OGJkNWQyYmM3YWVlYzVjNTkzNTkwODEzZTFkYTQzYTIyZDNhOTgzNSIsCiAgInZlcnNpb24iOiAxCn0=
  • 将上一步的结果放入到请求头中,例如 Authorization: ewogICJhY2Nlc3Nfa2V5I...24iOiAxCn0=

示例代码

为了更好地说明请求签名和身份认证的过程,下面提供一份 Python 的示例代码:

import base64
import hashlib
import hmac
import json
import time
from typing import Dict, List, Union
from urllib.parse import quote, urlencode, urlsplit

import requests


access_key = 'ac7418402ce0ce838ba87eb3a6be72af313cd7028e18007799c0d5651c326925'
secret_key = '5f0c5a5d51515947788fa7b8244acebe166aedd9de28b26ef716888a613c3d92'


def sign(
secret_key: str,
timestamp: Union[int, str],
method: str,
path: str,
headers: Dict[str, str],
query_params: Dict[str, Union[str, List[str]]],
body: bytes,
) -> str:
"""
参数说明:
- timestamp 是整数时间戳, 以秒为单位
- method 是 HTTP 请求方法, 例如 GET, POST, PUT, DELETE
- path 是 HTTP 请求路径, 注意不包括查询参数, 例如 /api/v1/regions
- headers 是 HTTP 请求头, 例如 {'Host': 'juicefs.com'}
- query_params 是 HTTP 请求的查询参数, 如果同一个字段对应多个值, 请使用列表来保存
例如 {'page': '1', 'per_page': '10', 'sort': ['name', 'created_at']}
- body 是 HTTP 请求的原始 body 内容, 对于大多数 HTTP 库来说, 可能需要先创建一个 Request 对象, 然后再从该对象中获取 body 内容
"""
# 1. 按顺序处理请求头, 字段名转为小写字符, 然后用 `:` 连接字段名和值, 最后用 `\n` 连接所有字段.
# 得到的结果形如 `host:juicefs.com`
sorted_headers = []
for h in ['Host']:
v = headers.get(h, '')
sorted_headers.append(f'{h.lower()}:{v}')
sorted_headers = '\n'.join(sorted_headers)

# 2. 对查询参数进行排序和编码
# 排序规则: 先按照字段名排序, 因为需要处理同一个字段对应多个值的情况, 所以还需要对字段的值进行排序
# 编码规则: 对字段名和值进行 URL 编码, 然后用 `=` 连接字段名和值, 最后用 `&` 连接所有字段
# 得到的结果形如 `a=1&a=2&b=3&c=4`
sorted_qs = sorted(query_params.items(), key=lambda item: item[0])
for i, (k, values) in enumerate(sorted_qs):
if not isinstance(values, list):
values = [values]
sorted_qs[i] = (k, sorted(values))
sorted_qs = urlencode(sorted_qs, doseq=True, quote_via=quote)

# 3. 对请求体进行 SHA256 哈希
payload_hash = hashlib.sha256(body).hexdigest() if body else ''

# 4. 用 `\n` 按顺序拼接上面的所有字符串
parts = [str(timestamp), method, path, sorted_headers, sorted_qs, payload_hash]
data = '\n'.join(parts)

# 5. 对拼接后的字符串进行 HMAC-SHA256 签名
signature = hmac.new(secret_key.encode('utf-8'), data.encode('utf-8'), hashlib.sha256).hexdigest()

return signature


def request(method: str, url: str, query_params: dict, data: dict):
timestamp = int(time.time())
parts = urlsplit(url)
path = parts.path
query_params = {}
headers = {'Host': parts.netloc}

req = requests.Request(method, url, params=query_params, json=data).prepare()
body = req.body or b''
signature = sign(secret_key, timestamp, method, path, headers, query_params, body)
print('signature:', signature)
auth = {'access_key': access_key, 'timestamp': timestamp, 'signature': signature, 'version': 1}
token = base64.b64encode(json.dumps(auth).encode()).decode()
req.headers['Authorization'] = token
resp = requests.Session().send(req)
print(json.dumps(resp.json(), indent=2))


# API 文档: https://juicefs.com/api/v1/docs


API_URL = 'https://juicefs.com/api/v1'


def get_nodes():
url = f'{API_URL}/nodes'
request('GET', url, {}, {})


def get_volumes():
url = f'{API_URL}/volumes'
request('GET', url, {}, {})


def update_volume():
url = f'{API_URL}/volumes/1'
request('POST', url, {}, {'name': 'fff123'})


def get_volume_exports():
url = f'{API_URL}/volumes/1/exports'
request('GET', url, {}, {})


def create_volume_export():
url = f'{API_URL}/volumes/1/exports'
request(
'POST',
url,
{},
{'desc': 'for mount', 'iprange': '192.168.0.1/24', 'apionly': False, 'readonly': False, 'appendonly': False},
)


def update_volume_export():
url = f'{API_URL}/volumes/1/exports/1'
request('PATCH', url, {}, {'desc': 'for mount', 'iprange': '192.168.100.1/24'})


def get_volume_quotas():
url = f'{API_URL}/volumes/1/quotas'
request('GET', url, {}, {})


def create_volume_quota():
url = f'{API_URL}/volumes/1/quotas'
request('POST', url, {}, {'path': '/path/to/subdir', 'inodes': 1 << 20, 'size': 1 << 30})


def update_volume_quota():
url = f'{API_URL}/volumes/1/quotas/1'
request('PATCH', url, {}, {'size': 10 << 30})