Console API
JuiceFS Web Console provides APIs for all file system interactions that users can perform through a web browser, such as creating file systems, browsing file lists, checking the trash directory, and configuring ACLs.
However, please note that the console cannot manipulate or modify files within the file system, so you cannot directly access or modify files within the file system through the console API. If you need programmatic access to the file system, consider using the S3 Gateway.
This document mainly introduces request signing and authentication. Check the API schema documentation to learn which APIs are supported by the console and the data format of the APIs.
URL and data format
For JuiceFS Cloud Service users, the API URL is https://juicefs.com/api/v1. For on-premises deployment users, you need to modify the URL accordingly, for example, http://console.example.com:8080/api/v1.
JSON is used as the data exchange format. Taking getting the file system list as an example, the request parameters are as follows:
GET /api/v1/volumes
Host: juicefs.com
Authorization: 4dca13af31e45740c1c1fe3acaca8752a093b43ccc169f890f1305f51f038bf8
...
The returned data is:
[
{
"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
}
]
Usage
Create an API key pair
- Click on your username in the top right corner of the console to enter the account settings page.
- Click Add new API key and fill in the information to create your key pair.
- Record the key pair immediately. After closing the dialog, you will not be able to view it again.
Request signing
Before sending an API request, sign the request using the API key pair.
For demonstration purposes, assume the created API key pair is:
- Access key:
ac7418402ce0ce838ba87eb3a6be72af313cd7028e18007799c0d5651c326925 - Secret key:
5f0c5a5d51515947788fa7b8244acebe166aedd9de28b26ef716888a613c3d92
Taking the request to create a file system as an example:
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"}
The a=1&a=2&b=3&c=4 query parameters in the request above are for illustrative purposes only and do not represent actual parameters. They are included to demonstrate the signing algorithm.
The signing algorithm is as follows:
-
Record the current timestamp. For this example, assume it is
1663245320. The server uses the timestamp to validate the request's freshness. If the timestamp differs from the server's local time by more than 5 minutes, the request is rejected. -
Process the request headers sequentially: convert field names to lowercase, join each field name and value with
:, and then join all fields with\n. Currently, only the host header is included in the calculation. The result ishost:juicefs.com. -
Sort and encode the query parameters. If no query parameters are present, use an empty string.
- Sorting rules: Sort by field name first. For fields with multiple values, sort the values as well.
- Encoding rules: URL-encode both field names and values, join each field name and value with
=, and then join all fields with&. Thea=1&a=2&b=3&c=4query parameters contain three fields:a,b, andc, where theafield has two values. After sorting by field name (resulting ina<b<c) and sorting the values for theafield, the final result isa=1&a=2&b=3&c=4.
-
Compute the SHA256 hash of the request body. If there is no request body, use an empty string.
The result is
a81f7bf3a5740146fe1eedc891f1f8f063dc428a88ac590147d1cf056bdad04b. -
Concatenate the timestamp, request method, request path, request headers, query parameters, and request body in order, using
\nas the separator.The result is:
1663245320\n
POST\n
/api/v1/volumes\n
host:juicefs.com\n
a=1&a=2&b=3&c=4\n
a81f7bf3a5740146fe1eedc891f1f8f063dc428a88ac590147d1cf056bdad04b -
Sign the concatenated string with HMAC-SHA256 with your secret key.
The result is
3646d11235b08cd856278cb68bd5d2bc7aeec5c593590813e1da43a22d3a9835note- The host field used in the signature must match the host header in the actual request. Most HTTP libraries allow you to explicitly set request headers, or you can construct a request object first and then extract the actual host from it.
- For request body data, you typically need to construct a request object first and then obtain the actual binary data from it.
Authentication
After calculating the signature, you need to generate a token for authentication using the access key, timestamp (the timestamp used in request signing), signature, and an version number.
The version number is optional. Currently, there is only one version; set it to 1.
The steps are as follows:
-
Combine these fields into JSON format data. The result is:
{
"access_key": "ac7418402ce0ce838ba87eb3a6be72af313cd7028e18007799c0d5651c326925",
"timestamp": 1663245320,
"signature": "3646d11235b08cd856278cb68bd5d2bc7aeec5c593590813e1da43a22d3a9835",
"version": 1
} -
Encode the JSON data from the previous step with base64. The result is:
ewogICJhY2Nlc3Nfa2V5IjogImFjNzQxODQwMmNlMGNlODM4YmE4N2ViM2E2YmU3MmFmMzEzY2Q3MDI4ZTE4MDA3Nzk5YzBkNTY1MWMzMjY5MjUiLAogICJ0aW1lc3RhbXAiOiAxNjYzMjQ1MzIwLAogICJzaWduYXR1cmUiOiAiMzY0NmQxMTIzNWIwOGNkODU2Mjc4Y2I2OGJkNWQyYmM3YWVlYzVjNTkzNTkwODEzZTFkYTQzYTIyZDNhOTgzNSIsCiAgInZlcnNpb24iOiAxCn0= -
Put the result from the previous step into the request header, for example,
Authorization: ewogICJhY2Nlc3Nfa2V5I...24iOiAxCn0=.
Code example
To better illustrate the process of request signing and authentication, example code in Python and Go is provided below:
- Python
- Go
import base64
import hashlib
import hmac
import json
import time
from typing import Dict, List, Literal, Optional, Union
from urllib.parse import quote_plus, urlencode, urlsplit
import requests
API_URL = 'http://localhost:8080/api/v1'
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:
"""
Parameter description:
- timestamp: Integer timestamp in seconds
- method: HTTP request method, e.g., GET, POST, PUT, DELETE
- path: HTTP request path without query parameters, e.g., /api/v1/regions
- headers: HTTP request headers, e.g., {'Host': 'juicefs.com'}
- query_params: Query parameters of the HTTP request. If the same field corresponds to multiple values, please use a list to save them. For example, {'page': '1', 'per_page': '10', 'sort': ['name', 'created_at']}.
- body: Raw body content of the HTTP request. or most HTTP libraries, you may need to create a request object and then get the body content from it.
"""
# 1. Process request headers in order: convert field names to lowercase, format as 'name:value', and join all fields with `\n`.
# The result looks like `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. Sort and encode query parameters.
# Sorting rules: sort by field name. For multi-value fields, sort by values.
# Encoding rules: URL-encode the field names and values, join the field names and values with `=`, and join all fields with `&`.
# The result looks like `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, safe='', quote_via=quote_plus)
# 3. Calculate body hash with SHA256.
payload_hash = hashlib.sha256(body).hexdigest() if body else ''
# 4. Concatenate all the above strings in order with `\n`.
parts = [str(timestamp), method, path, sorted_headers, sorted_qs, payload_hash]
data = '\n'.join(parts)
# 5. Sign the concatenated string with 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('response:')
print(json.dumps(resp.json(), indent=2))
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})
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
)
const (
API_URL = "http://localhost:8080/api/v1"
ACCESS_KEY = "ac7418402ce0ce838ba87eb3a6be72af313cd7028e18007799c0d5651c326925"
SECRET_KEY = "5f0c5a5d51515947788fa7b8244acebe166aedd9de28b26ef716888a613c3d92"
)
// Parameter description:
//
// timestamp: Integer timestamp in seconds
// method: HTTP request method, e.g., GET, POST, PUT, DELETE
// path: HTTP request path without query parameters, e.g., /api/v1/regions
// headers: HTTP request headers, e.g., {'Host': 'juicefs.com'}
// query_params: Query parameters of the HTTP request. If the same field corresponds to multiple values, please use a list to save them. For example, {'page': '1', 'per_page': '10', 'sort': ['name', 'created_at']}.
// body: Raw body content of the HTTP request. or most HTTP libraries, you may need to create a request object and then get the body content from it.
func sign(
secretKey string,
timestamp int64,
method string,
path string,
headers http.Header,
queryParams url.Values,
body []byte,
) (string, error) {
// 1. Process request headers in order: convert field names to lowercase, format as 'name:value', and join all fields with `\n`.
// The result looks like `host:juicefs.com`
sortedHeaders := []string{}
for _, h := range []string{"Host"} {
v := headers.Get(h)
if v == "" {
return "", fmt.Errorf("header %s is required", h)
}
sortedHeaders = append(sortedHeaders, fmt.Sprintf("%s:%s", strings.ToLower(h), v))
}
srotedHeadersString := strings.Join(sortedHeaders, "\n")
// 2. Sort and encode query parameters.
// Sorting rules: sort by field name. For multi-value fields, sort by values.
// Encoding rules: URL-encode the field names and values, join the field names and values with `=`, and join all fields with `&`.
// The result looks like `a=1&a=2&b=3&c=4`.
sortedQueryString := ""
if queryParams != nil {
sortedKeys := make([]string, 0, len(queryParams))
for k := range queryParams {
sortedKeys = append(sortedKeys, k)
}
sort.Strings(sortedKeys)
sortedQueryParams := make([]string, 0, len(queryParams))
for _, k := range sortedKeys {
sort.Strings(queryParams[k])
for _, value := range queryParams[k] {
sortedQueryParams = append(sortedQueryParams, fmt.Sprintf("%s=%s", url.QueryEscape(k), url.QueryEscape(value)))
}
}
sortedQueryString = strings.Join(sortedQueryParams, "&")
}
// 3. Calculate body hash with SHA256.
payloadHash := ""
if body != nil {
hash := sha256.New()
hash.Write(body)
payloadHash = hex.EncodeToString(hash.Sum(nil))
}
// 4. Concatenate all the above strings in order with `\n`.
parts := []string{
fmt.Sprintf("%d", timestamp),
method,
path,
srotedHeadersString,
sortedQueryString,
payloadHash,
}
data := strings.Join(parts, "\n")
// 5. Sign the concatenated string with HMAC-SHA256.
hash := hmac.New(sha256.New, []byte(secretKey))
hash.Write([]byte(data))
signature := hex.EncodeToString(hash.Sum(nil))
return signature, nil
}
func request(
method string,
api_url string,
queryParams url.Values,
data map[string]interface{},
) error {
var (
body []byte
err error
json_data []byte
)
timestamp := time.Now().Unix()
api_url = fmt.Sprintf("%s?%s", api_url, queryParams.Encode())
if data != nil {
body, err = json.Marshal(data)
if err != nil {
return err
}
}
req, err := http.NewRequest(method, api_url, bytes.NewBuffer(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Host", req.URL.Host)
fmt.Printf("secret_key: %s...%s\n", SECRET_KEY[:10], SECRET_KEY[len(SECRET_KEY)-10:])
fmt.Printf("timestamp: %d\n", timestamp)
fmt.Printf("method: %s\n", method)
fmt.Printf("path: %s\n", req.URL.Path)
json_data, err = json.Marshal(req.Header)
if err != nil {
return err
}
fmt.Printf("headers: %s\n", string(json_data))
fmt.Printf("query_params: %v\n", req.URL.RawQuery)
fmt.Printf("body: %s\n", body)
signature, err := sign(SECRET_KEY, timestamp, method, req.URL.Path, req.Header, queryParams, body)
if err != nil {
return err
}
fmt.Printf("signature: %s\n", signature)
auth := map[string]interface{}{
"access_key": ACCESS_KEY,
"timestamp": timestamp,
"signature": signature,
"version": 1,
}
jsonString, err := json.Marshal(auth)
if err != nil {
return err
}
token := base64.StdEncoding.EncodeToString(jsonString)
fmt.Printf("token: %s\n", token)
req.Header.Set("Authorization", token)
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
resp_body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
fmt.Printf("response: \n%s\n", string(resp_body))
return nil
}
func getVolumes() error {
u := fmt.Sprintf("%s/volumes", API_URL)
return request("GET", u, nil, nil)
}
func deleteVolume() error {
u := fmt.Sprintf("%s/volumes/1", API_URL)
return request("DELETE", u, nil, nil)
}
func getVolumeExports() error {
u := fmt.Sprintf("%s/volumes/1/exports", API_URL)
return request("GET", u, nil, nil)
}
func createVolumeExport() error {
u := fmt.Sprintf("%s/volumes/1/exports", API_URL)
return request(
"POST",
u,
nil,
map[string]interface{}{
"desc": "for mount",
"iprange": "192.168.0.1/24",
"apionly": false,
"readonly": false,
"appendonly": false,
},
)
}
func updateVolumeExport() error {
u := fmt.Sprintf("%s/volumes/1/exports/1", API_URL)
return request("PUT", u, nil, map[string]interface{}{"desc": "abc", "iprange": "192.168.100.1/24"})
}
func getVolumeQuotas() error {
u := fmt.Sprintf("%s/volumes/1/quotas", API_URL)
return request("GET", u, nil, nil)
}
func createVolumeQuota() error {
u := fmt.Sprintf("%s/volumes/1/quotas", API_URL)
return request(
"POST",
u,
nil,
map[string]interface{}{"path": "/path/to/subdir", "inodes": 1 << 20, "size": 1 << 30},
)
}
func updateVolumeQuota() error {
u := fmt.Sprintf("%s/volumes/1/quotas/1", API_URL)
return request("PUT", u, nil, map[string]interface{}{"path": "/foo", "size": 10 << 30})
}
func main() {
if err := getVolumes(); err != nil {
fmt.Printf("request error: %s", err)
}
}

