简易对象存储服务 · 文件上传至项目根目录 /zfss · 返回可外网访问的 URL
Z-FSS(Zhuolu File Storage Service)是一套基于 Laravel 11 + LayUI + MySQL 的轻量文件存储系统,类似阿里云 OSS / MinIO 的精简版。
zfss/{bucket}/...,并同步到 public/zfss/http://z-fss.zhuoluyun.com/zfss/default/images/2026/xxx.pdf以下类型参考阿里云 OSS 常见对象格式。上传时以文件扩展名为主校验;MIME 类型也会辅助校验(允许 application/octet-stream)。
| 分类 | 支持的扩展名 |
|---|---|
| 图片 | jpg, jpeg, png, gif, bmp, webp, svg, ico, tif, tiff, heic, heif, psd, ai, eps |
| 文档 / 办公 | pdf, doc, docx, xls, xlsx, ppt, pptx, txt, rtf, csv, md, odt, ods, odp, pages, numbers, key |
| 压缩包 | zip, rar, 7z, tar, gz, tgz, bz2, xz |
| 音频 | mp3, wav, aac, flac, ogg, m4a, wma, amr, ape |
| 视频 | mp4, avi, mov, wmv, flv, mkv, webm, m3u8, ts, 3gp, mpg, mpeg, rm, rmvb, m4v |
| 网页 / 开发资源 | html, htm, css, js, json, xml, yaml, yml, map, wasm |
| 字体 | ttf, otf, woff, woff2, eot |
| 安装包 / 其他 | apk, ipa, dmg, iso, img, bin, dat, log, sql, conf, ini, pem, crt, cer, p12 |
合计:共支持 94 种扩展名。
即使未在白名单中,以下类型也会被拒绝(防止可执行脚本通过 Web 目录被访问):
php, phtml, php3, php4, php5, php7, php8, phar, asp, aspx, asa, asax, ascx, ashx, asmx, jsp, jspx, cgi, htaccess, htpasswd, shtml, exe, bat, cmd, com, msi, scr, vbs, ps1, sh
如需增减类型,请修改服务器上的 config/zfss.php,并执行 php artisan config:clear。
登录管理后台 → API 密钥 → 创建密钥,获得 AccessKey 与 SecretKey。
http://z-fss.zhuoluyun.com/api/v1
方式 A:请求头(推荐)
X-Access-Key: 你的AccessKey
X-Secret-Key: 你的SecretKey
方式 B:Authorization Bearer
Authorization: Bearer {AccessKey}:{SecretKey}
方式 C:Query / 表单参数
access_key=你的AccessKey&secret_key=你的SecretKey
{
"code": 0,
"message": "success",
"data": { ... }
}
| 字段 | 说明 |
|---|---|
| code | 0 表示成功,非 0 为错误码(与 HTTP 状态码接近) |
| message | 提示信息 |
| data | 业务数据,失败时为 null |
文件按 Bucket 组织,可在 Bucket 内创建嵌套目录(前缀)。未指定时使用默认 Bucket default。
物理路径:zfss/{bucket}/{path}/文件名;公网 URL:http://z-fss.zhuoluyun.com/zfss/{bucket}/{path}/文件名。
GET http://z-fss.zhuoluyun.com/api/v1/buckets
GET http://z-fss.zhuoluyun.com/api/v1/buckets?all=1 # 含已禁用
POST http://z-fss.zhuoluyun.com/api/v1/buckets
Content-Type: application/x-www-form-urlencoded
name=my-bucket&display_name=业务桶&remark=可选备注
名称规则:1–63 位小写字母、数字、连字符,不能以连字符开头/结尾。
POST http://z-fss.zhuoluyun.com/api/v1/directories
Content-Type: application/x-www-form-urlencoded
bucket=default&path=images/2026
path / prefix / key 等价;可嵌套,父目录会自动创建。上传文件时也会按 OSS 习惯自动创建目录记录,一般无需单独调用本接口。
GET http://z-fss.zhuoluyun.com/api/v1/directories?bucket=default&prefix=images
POST http://z-fss.zhuoluyun.com/api/v1/upload
Content-Type: multipart/form-data
| 参数 | 位置 | 必填 | 说明 |
|---|---|---|---|
| file | form-data | 是 | 文件字段名必须为 file(类似 OSS PostObject),类型见第 2 节 |
| bucket | form-data | 否 | Bucket 名称,默认 default;空字符串同样回落到默认桶 |
| path / prefix / key | form-data | 否 | 目录前缀(Object Key Prefix),如 images/2026/;三者等价,取先非空者。无需事先 mkdir,上传时自动创建目录记录。省略则按日期目录 Y/m/d 存放 |
| X-Access-Key | Header | 是 | AccessKey |
| X-Secret-Key | Header | 是 | SecretKey |
公网 URL 形如 http://z-fss.zhuoluyun.com/zfss/{bucket}/{relative};响应中的 object_key / relative_path 为 Bucket 内相对对象键(含桶名前缀)。
图片自动压缩(默认开启):当上传文件为图片且体积 > ZFSS_IMAGE_MAX_KB(默认 1024 KB)时,服务端在最终落库前用 PHP GD(优先)或 Imagick 压缩到 ≤ 阈值;API / 管理后台简单上传与 multipart/complete 均生效。压缩范围:jpeg、png、webp、静态 gif(不透明 PNG 可能转为 jpeg);跳过:svg、ico、动画 gif,以及 bmp/tiff/heic 等。非图片文件不变。关闭:ZFSS_IMAGE_COMPRESS=false。
成功响应示例:
{
"code": 0,
"message": "上传成功",
"data": {
"uuid": "a1b2c3d4-....",
"url": "http://z-fss.zhuoluyun.com/zfss/default/images/2026/20261022153000_abcdef.pdf",
"object_key": "default/images/2026/20261022153000_abcdef.pdf",
"original_name": "contract.pdf",
"size": 204800,
"mime_type": "application/pdf",
"extension": "pdf",
"bucket": "default",
"directory_path": "images/2026",
"relative_path": "default/images/2026/20261022153000_abcdef.pdf",
"created_at": "2026-10-22 15:30:00"
}
}
当文件大小 大于 5 MB(5120 KB,配置项 ZFSS_MULTIPART_THRESHOLD_KB)时,应使用分片上传;≤ 阈值仍用上方单次 POST /upload。建议分片大小 5 MB(ZFSS_MULTIPART_PART_SIZE_KB),末片可更小。鉴权 Header 与普通上传相同。
POST http://z-fss.zhuoluyun.com/api/v1/multipart/init
Content-Type: multipart/form-data 或 application/x-www-form-urlencoded
| 参数 | 必填 | 说明 |
|---|---|---|
| filename | 是 | 原始文件名(含扩展名) |
| size | 是 | 文件总字节数 |
| mime / mime_type | 否 | MIME 类型 |
| bucket | 否 | 默认 default |
| path / prefix / key | 否 | 目录前缀,规则同单文件上传 |
成功返回 upload_id、object_key(计划对象键)、part_size、expected_parts 等。
POST http://z-fss.zhuoluyun.com/api/v1/multipart/upload
Content-Type: multipart/form-data
upload_id=...&part_number=1&file=@part_blob
part_number 从 1 开始;临时文件保存在 storage/app/multipart/{upload_id}/part_{n}。
POST http://z-fss.zhuoluyun.com/api/v1/multipart/complete
upload_id=...
parts=1,2,3 # 可选;省略则合并全部已上传分片
合并写入 zfss/{bucket}/...,创建 FileRecord,同步 public/zfss/,清理临时目录。响应字段与单文件上传一致(url、uuid、object_key 等)。
POST http://z-fss.zhuoluyun.com/api/v1/multipart/abort
upload_id=...
GET http://z-fss.zhuoluyun.com/api/v1/files?page=1&limit=20&bucket=default&prefix=images&keyword=
bucket、prefix/path 可选,用于按桶与目录前缀过滤。
GET http://z-fss.zhuoluyun.com/api/v1/files/{uuid}
DELETE http://z-fss.zhuoluyun.com/api/v1/files/{uuid}
以下均为可直接复制的简单上传示例:POST /api/v1/upload,表单字段 file(必填)、bucket / path(可选,类 OSS),鉴权 Header 为 X-Access-Key / X-Secret-Key。
文件大小 大于 5 MB 时请改用分片上传(见 第 6.1 节 与本节末尾);≤ 阈值用下方单次上传即可。在线表单联调见 /docs/demo。
curl -X POST "http://z-fss.zhuoluyun.com/api/v1/upload" \
-H "X-Access-Key: 你的AccessKey" \
-H "X-Secret-Key: 你的SecretKey" \
-F "file=@/path/to/document.pdf" \
-F "bucket=default" \
-F "path=images/2026"
use Illuminate\Support\Facades\Http;
// 简单上传(≤ 5 MB)
$response = Http::withHeaders([
'X-Access-Key' => '你的AccessKey',
'X-Secret-Key' => '你的SecretKey',
])->attach('file', file_get_contents('/path/to/document.pdf'), 'document.pdf')
->post('http://z-fss.zhuoluyun.com/api/v1/upload', [
'bucket' => 'default',
'path' => 'images/2026',
]);
$data = $response->json();
$url = $data['data']['url'] ?? null; // 公网 URL
$objectKey = $data['data']['object_key'] ?? null; // 桶内对象键
// 若 filesize > 5120 * 1024,应改走 multipart:
// POST /api/v1/multipart/init → /multipart/upload(各 part)→ /multipart/complete
// 流程与下方「分片上传」JS / Laravel 草图一致,详见第 6.1 节
<?php
$ch = curl_init('http://z-fss.zhuoluyun.com/api/v1/upload');
$filePath = '/path/to/document.pdf';
$cfile = new CURLFile($filePath, mime_content_type($filePath) ?: 'application/octet-stream', basename($filePath));
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-Access-Key: 你的AccessKey',
'X-Secret-Key: 你的SecretKey',
],
CURLOPT_POSTFIELDS => [
'file' => $cfile,
'bucket' => 'default',
'path' => 'images/2026',
],
]);
$body = curl_exec($ch);
if ($body === false) {
throw new RuntimeException(curl_error($ch));
}
curl_close($ch);
$json = json_decode($body, true);
echo $json['data']['url'] ?? '';
echo $json['data']['object_key'] ?? '';
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
RestTemplate rest = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.set("X-Access-Key", "你的AccessKey");
headers.set("X-Secret-Key", "你的SecretKey");
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", new FileSystemResource("/path/to/document.pdf"));
body.add("bucket", "default");
body.add("path", "images/2026");
HttpEntity<MultiValueMap<String, Object>> req = new HttpEntity<>(body, headers);
ResponseEntity<String> res = rest.postForEntity(
"http://z-fss.zhuoluyun.com/api/v1/upload", req, String.class);
System.out.println(res.getBody());
// 大文件(> 5 MB)请改用 WebClient/OkHttp 调用
// /api/v1/multipart/init|upload|complete,见第 6.1 节
import requests
url = 'http://z-fss.zhuoluyun.com/api/v1/upload'
headers = {
'X-Access-Key': '你的AccessKey',
'X-Secret-Key': '你的SecretKey',
}
# 使用 with 确保文件句柄关闭
with open('/path/to/document.pdf', 'rb') as f:
files = {'file': ('document.pdf', f)}
data = {'bucket': 'default', 'path': 'images/2026'}
r = requests.post(url, headers=headers, files=files, data=data)
r.raise_for_status()
payload = r.json()
print(payload['data']['url'])
print(payload['data']['object_key'])
# 文件大于 5 MB 时请走 multipart 接口(第 6.1 节)
package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
)
func main() {
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
_ = w.WriteField("bucket", "default")
_ = w.WriteField("path", "images/2026")
filePath := "/path/to/document.pdf"
fw, err := w.CreateFormFile("file", filepath.Base(filePath))
if err != nil {
panic(err)
}
f, err := os.Open(filePath)
if err != nil {
panic(err)
}
defer f.Close()
if _, err = io.Copy(fw, f); err != nil {
panic(err)
}
_ = w.Close()
req, err := http.NewRequest(http.MethodPost, "http://z-fss.zhuoluyun.com/api/v1/upload", &buf)
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", w.FormDataContentType())
req.Header.Set("X-Access-Key", "你的AccessKey")
req.Header.Set("X-Secret-Key", "你的SecretKey")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
// 大文件(> 5 MB)请分片调用 multipart 接口
}
const form = new FormData();
form.append('file', fileInput.files[0]); // 或 Blob / File
form.append('bucket', 'default'); // 可选,默认 default
form.append('path', 'images/2026/'); // 可选目录前缀(亦可用 prefix / key)
const res = await fetch('http://z-fss.zhuoluyun.com/api/v1/upload', {
method: 'POST',
headers: {
'X-Access-Key': '你的AccessKey',
'X-Secret-Key': '你的SecretKey'
// 不要手动设置 Content-Type,由浏览器带 boundary
},
body: form
});
const json = await res.json();
console.log(json.data.url, json.data.object_key);
流程:POST /api/v1/multipart/init → 循环 /multipart/upload(part_number 从 1 起)→ /multipart/complete(可选 /abort)。参数与响应见 第 6.1 节。下方给出 cURL、浏览器 JS 完整示例,以及 Laravel 分片草图;其他语言可按相同三步用各自的 multipart HTTP 客户端实现。
# 1) init
curl -X POST "http://z-fss.zhuoluyun.com/api/v1/multipart/init" \
-H "X-Access-Key: 你的AccessKey" \
-H "X-Secret-Key: 你的SecretKey" \
-F "filename=big.bin" -F "size=6291456" \
-F "bucket=default" -F "path=images/2026"
# 2) upload parts(part_number 从 1 起)
curl -X POST "http://z-fss.zhuoluyun.com/api/v1/multipart/upload" \
-H "X-Access-Key: 你的AccessKey" \
-H "X-Secret-Key: 你的SecretKey" \
-F "upload_id=UPLOAD_ID" -F "part_number=1" -F "file=@part1.bin"
# 3) complete
curl -X POST "http://z-fss.zhuoluyun.com/api/v1/multipart/complete" \
-H "X-Access-Key: 你的AccessKey" \
-H "X-Secret-Key: 你的SecretKey" \
-F "upload_id=UPLOAD_ID"
const THRESHOLD = 5120 * 1024; // 5 MB
const PART = 5120 * 1024;
const headers = { 'X-Access-Key': '你的AccessKey', 'X-Secret-Key': '你的SecretKey' };
const base = 'http://z-fss.zhuoluyun.com/api/v1';
async function multipartUpload(file) {
const initFd = new FormData();
initFd.append('filename', file.name);
initFd.append('size', String(file.size));
initFd.append('bucket', 'default');
initFd.append('path', 'images/2026');
const init = await (await fetch(base + '/multipart/init', { method: 'POST', headers, body: initFd })).json();
if (init.code !== 0) throw new Error(init.message);
const uploadId = init.data.upload_id;
const partSize = init.data.part_size || PART;
const total = Math.ceil(file.size / partSize);
for (let n = 1; n <= total; n++) {
const blob = file.slice((n - 1) * partSize, Math.min(file.size, n * partSize));
const fd = new FormData();
fd.append('upload_id', uploadId);
fd.append('part_number', String(n));
fd.append('file', blob, 'part_' + n);
const partRes = await (await fetch(base + '/multipart/upload', { method: 'POST', headers, body: fd })).json();
if (partRes.code !== 0) throw new Error(partRes.message);
}
const doneFd = new FormData();
doneFd.append('upload_id', uploadId);
return (await fetch(base + '/multipart/complete', { method: 'POST', headers, body: doneFd })).json();
}
if (file.size > THRESHOLD) {
console.log(await multipartUpload(file));
} else {
// 走普通 POST /upload(见 7.7)
}
use Illuminate\Support\Facades\Http;
$headers = [
'X-Access-Key' => '你的AccessKey',
'X-Secret-Key' => '你的SecretKey',
];
$base = 'http://z-fss.zhuoluyun.com/api/v1';
$path = '/path/to/big.bin';
$size = filesize($path);
$partSize = 5120 * 1024; // 建议分片大小
// 1) init
$init = Http::withHeaders($headers)->asMultipart()->post($base.'/multipart/init', [
['name' => 'filename', 'contents' => basename($path)],
['name' => 'size', 'contents' => (string) $size],
['name' => 'bucket', 'contents' => 'default'],
['name' => 'path', 'contents' => 'images/2026'],
])->json();
$uploadId = $init['data']['upload_id'];
$partSize = $init['data']['part_size'] ?? $partSize;
// 2) 按 part 切分上传(省略:fopen + fread 循环 part_number)
// Http::withHeaders($headers)->attach('file', $chunk, 'part_N')
// ->post($base.'/multipart/upload', ['upload_id' => $uploadId, 'part_number' => $n]);
// 3) complete
$done = Http::withHeaders($headers)->asMultipart()->post($base.'/multipart/complete', [
['name' => 'upload_id', 'contents' => $uploadId],
])->json();
$url = $done['data']['url'] ?? null;
z-fss.zhuoluyunkeji.com)指向 public/ 目录.env 中的 APP_URL=https://z-fss.zhuoluyunkeji.com 与 MySQL 连接php artisan migrate --seed 初始化数据库zfss/ 与 public/zfss/ 可写默认管理员:admin@zfss.local / admin123
更多细节见项目根目录 README.md。