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 密钥对为:

  • 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, Literal, Optional, Union
from urllib.parse import quote, urlencode, urlsplit

import requests


access_key = 'ac7418402ce0ce838ba87eb3a6be72af313cd7028e18007799c0d5651c326925'
secret_key = '5f0c5a5d51515947788fa7b8244acebe166aedd9de28b26ef716888a613c3d92'


def sign(
secret_key: str,
timestamp: int,
method: Literal['GET', 'POST', 'PATCH', 'PUT', 'DELETE'],
path: str,
headers: Dict[str, str],
query_params: Optional[Dict[str, Union[str, List[str]]]],
body: Optional[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, '')
if not v:
raise ValueError(f'header {h} is required')
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 = ''
if query_params:
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: Literal['GET', 'POST', 'PATCH', 'PUT', 'DELETE'],
url: str,
query_params: Optional[dict] = None,
data: Optional[dict] = None,
):
timestamp = int(time.time())
parts = urlsplit(url)
path = parts.path
headers = {'Host': parts.netloc}

req = requests.Request(method, url, params=query_params, json=data).prepare()
body = req.body or b''

print(f'secret_key: {secret_key[:10]}...{secret_key[-10:]}')
print(f'timestamp: {timestamp}')
print(f'method: {method}')
print(f'path: {path}')
print(f'headers: {headers}')
print(f'query_params: {query_params if query_params else "empty"}')
print(f'body: {body if body else "empty"}')

signature = sign(secret_key, timestamp, method, path, headers, query_params, body)
print(f'signature: {signature}')

auth = {
'access_key': access_key,
'timestamp': timestamp,
'signature': signature,
'version': 1,
}
token = base64.b64encode(json.dumps(auth).encode()).decode()
print(f'token: {token}')

req.headers['Authorization'] = token
resp = requests.Session().send(req)

print()
print(json.dumps(resp.json(), indent=2))


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


API_URL = 'http://localhost:8080/api/v1'


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


def delete_volume():
url = f'{API_URL}/volumes/1'
request('DELETE', url)


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,
data={
'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('PUT', url, data={'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,
data={'path': '/path/to/subdir', 'inodes': 1 << 20, 'size': 1 << 30},
)


def update_volume_quota():
url = f'{API_URL}/volumes/1/quotas/9'
request('PUT', url, data={'path': '/foo', 'size': 10 << 30})