Z-FSS 开发文档

简易对象存储服务 · 文件上传至项目根目录 /zfss · 返回可外网访问的 URL

1. 概述

Z-FSS(Zhuolu File Storage Service)是一套基于 Laravel 11 + LayUI + MySQL 的轻量文件存储系统,类似阿里云 OSS / MinIO 的精简版。

2. 支持的文件类型

以下类型参考阿里云 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。

3. 快速开始

3.1 获取密钥

登录管理后台 → API 密钥 → 创建密钥,获得 AccessKey 与 SecretKey。

3.2 Base URL

http://z-fss.zhuoluyun.com/api/v1

3.3 鉴权方式(任选其一)

方式 A:请求头(推荐)

X-Access-Key: 你的AccessKey
X-Secret-Key: 你的SecretKey

方式 B:Authorization Bearer

Authorization: Bearer {AccessKey}:{SecretKey}

方式 C:Query / 表单参数

access_key=你的AccessKey&secret_key=你的SecretKey

4. 统一响应格式

{
  "code": 0,
  "message": "success",
  "data": { ... }
}
字段说明
code0 表示成功,非 0 为错误码(与 HTTP 状态码接近)
message提示信息
data业务数据,失败时为 null

5. Bucket 与目录

文件按 Bucket 组织,可在 Bucket 内创建嵌套目录(前缀)。未指定时使用默认 Bucket default。

物理路径:zfss/{bucket}/{path}/文件名;公网 URL:http://z-fss.zhuoluyun.com/zfss/{bucket}/{path}/文件名。

GET 列出 Bucket

GET http://z-fss.zhuoluyun.com/api/v1/buckets
GET http://z-fss.zhuoluyun.com/api/v1/buckets?all=1   # 含已禁用

POST 创建 Bucket

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 创建目录

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 列出目录

GET http://z-fss.zhuoluyun.com/api/v1/directories?bucket=default&prefix=images

6. 文件接口

POST 上传文件

POST http://z-fss.zhuoluyun.com/api/v1/upload
Content-Type: multipart/form-data
参数位置必填说明
fileform-data是文件字段名必须为 file(类似 OSS PostObject),类型见第 2 节
bucketform-data否Bucket 名称,默认 default;空字符串同样回落到默认桶
path / prefix / keyform-data否目录前缀(Object Key Prefix),如 images/2026/;三者等价,取先非空者。无需事先 mkdir,上传时自动创建目录记录。省略则按日期目录 Y/m/d 存放
X-Access-KeyHeader是AccessKey
X-Secret-KeyHeader是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"
  }
}

6.1 分片上传(Multipart,类 OSS)

当文件大小 大于 5 MB(5120 KB,配置项 ZFSS_MULTIPART_THRESHOLD_KB)时,应使用分片上传;≤ 阈值仍用上方单次 POST /upload。建议分片大小 5 MB(ZFSS_MULTIPART_PART_SIZE_KB),末片可更小。鉴权 Header 与普通上传相同。

POST 初始化

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 上传分片

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 合并完成

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 中止(可选)

POST http://z-fss.zhuoluyun.com/api/v1/multipart/abort

upload_id=...

GET 文件列表

GET http://z-fss.zhuoluyun.com/api/v1/files?page=1&limit=20&bucket=default&prefix=images&keyword=

bucket、prefix/path 可选,用于按桶与目录前缀过滤。

GET 文件详情

GET http://z-fss.zhuoluyun.com/api/v1/files/{uuid}

DELETE 删除文件

DELETE http://z-fss.zhuoluyun.com/api/v1/files/{uuid}

7. 多语言对接示例

以下均为可直接复制的简单上传示例:POST /api/v1/upload,表单字段 file(必填)、bucket / path(可选,类 OSS),鉴权 Header 为 X-Access-Key / X-Secret-Key。

文件大小 大于 5 MB 时请改用分片上传(见 第 6.1 节 与本节末尾);≤ 阈值用下方单次上传即可。在线表单联调见 /docs/demo。

7.1 cURL

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"

7.2 Laravel(Http Facade)

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 节

7.3 PHP 无框架(cURL 扩展)

<?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'] ?? '';

7.4 Spring Boot(RestTemplate multipart)

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 节

7.5 Python(requests)

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 节)

7.6 Go(net/http multipart)

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 接口
}

7.7 JavaScript(fetch,浏览器 / Node)

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);

7.8 分片上传说明与示例(> 5 MB)

流程:POST /api/v1/multipart/init → 循环 /multipart/upload(part_number 从 1 起)→ /multipart/complete(可选 /abort)。参数与响应见 第 6.1 节。下方给出 cURL、浏览器 JS 完整示例,以及 Laravel 分片草图;其他语言可按相同三步用各自的 multipart HTTP 客户端实现。

cURL

# 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"

JavaScript(浏览器,完整)

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)
}

Laravel 分片草图(> 5 MB)

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;

8. 部署说明(自有域名)

  1. 将代码部署到服务器,Nginx/Apache 将域名(如 z-fss.zhuoluyunkeji.com)指向 public/ 目录
  2. 配置 .env 中的 APP_URL=https://z-fss.zhuoluyunkeji.com 与 MySQL 连接
  3. 执行 php artisan migrate --seed 初始化数据库
  4. 确保项目根目录 zfss/ 与 public/zfss/ 可写
  5. 生产环境请修改默认管理员密码并重新创建 API 密钥

默认管理员:admin@zfss.local / admin123

更多细节见项目根目录 README.md。