flytachi/winter-s4w-api
Composer 安装命令:
composer require flytachi/winter-s4w-api
包简介
S4W file storage API client for the Winter framework — upload, list, fetch, delete and private streaming proxy.
README 文档
README
PHP client for S4W — a self-hosted file storage service — built on top of the Winter Framework HTTP layer (flytachi/winter-cast).
It wraps the S4W REST API behind a small, typed facade: upload, list, fetch and
delete files, build private/public URLs, and stream private files straight
through your backend to the browser (with HTTP Range / seek support) without
ever exposing the master token.
$file = S4w::fileUpload('/tmp/invoice.pdf', section: 'documents'); $file->id; // "692320e4-ebaa-49ae-8399-abced0ea44c8" $file->mime; // "application/pdf" $file->isPublic; // false
What is S4W?
S4W is a standalone file-storage server (deduplicating object store with public and private access, sections, image compression / WebP conversion).
- Source: https://github.com/Flytachi/s4w
- Docker image:
flytachi/s4w
docker run -d --name s4w -p 9090:80 flytachi/s4w
This package is the client; it talks to a running S4W instance over HTTP.
Requirements
- PHP >= 8.3
ext-curl- flytachi/winter-cast ^1.0
- A reachable S4W instance + its API token
Installation
composer require flytachi/winter-s4w-api
Configuration
The package ships an abstract base class, S4wBaseApi. You create one
concrete subclass that supplies the connection details. This keeps credentials in
your app (env, config, secrets manager) and lets you have multiple S4W
connections if needed.
<?php namespace App\Services; use Flytachi\Winter\S4w\S4wBaseApi; class S4w extends S4wBaseApi { protected static function baseUrl(): string { return env('S4W_URL'); // e.g. https://files.example.com } protected static function token(): string { return env('S4W_TOKEN'); // master/instance token (server-to-server only) } protected static function instance(): string { return env('S4W_INSTANCE'); // instance slug, used to build PUBLIC urls } }
# .env S4W_URL=https://files.example.com S4W_TOKEN=your-instance-master-token S4W_INSTANCE=my-app
| Method | Required for | Notes |
|---|---|---|
baseUrl() |
everything | Base URL of the S4W instance. Trailing slash optional. |
token() |
everything | Bearer token. Server-to-server only — never sent to the browser. |
instance() |
generateUrls*() only |
Public-URL slug. Throws if empty when building public URLs. |
The
token()is your master key to the instance. The whole point ofstreamPrivate()is that private files reach the user through your backend, so this token never leaves the server.
Concepts
- Section — an optional namespace/folder for files (
documents,avatars, …). Sections can be public or private (sectionList()reportspublic). - Private vs public access:
- Private →
/p/[section/]{id}— requires the token. Served by your backend viastreamPrivate()after you authorize the request. - Public →
/o/{instance}/[section/]{id}— directly reachable, no token.
- Private →
S4wFile— immutable DTO returned by upload/get/list.PResult/PMeta— page-centric pagination envelope ({ meta, data }).
API reference
All methods are static and called on your subclass (S4w below).
sectionList(): array
Returns the list of sections: [{ name: string, public: bool }, …] (or []).
$sections = S4w::sectionList(); // [['name' => 'documents', 'public' => false], ['name' => 'avatars', 'public' => true]]
fileList(int $limit = 20, int $page = 1, string $section = '', string $search = ''): PResult<S4wFile>
Paginated listing. Empty $section / $search are omitted from the query.
$result = S4w::fileList(limit: 50, page: 1, section: 'documents', search: 'invoice'); $result->meta->total; // 134 $result->meta->pages; // 3 $result->meta->next; // 2 | null foreach ($result->data as $file) { echo $file->name; // each is an S4wFile }
fileUpload(string $filePath, string $name = '', string $section = '', ?int $compress = null, bool $webp = false): S4wFile
Uploads a local file (multipart). Throws InvalidArgumentException if the path is
not a readable file, or if $compress is outside 0..100.
$file = S4w::fileUpload( filePath: '/tmp/photo.png', name: 'avatar.png', // optional override of the stored name section: 'avatars', compress: 80, // 0..100, optional webp: true, // convert to WebP );
fileGet(string $id): S4wFile
Fetches a single file's metadata.
$file = S4w::fileGet('692320e4-ebaa-49ae-8399-abced0ea44c8');
fileDelete(string $id): void
Deletes a file.
S4w::fileDelete('692320e4-ebaa-49ae-8399-abced0ea44c8');
generateUrls(string ...$ids): array<string, S4wUrls>
Builds private + public URLs for one or more ids, keyed by id. Requires
instance() to be set (for the public URL).
$urls = S4w::generateUrls('id-a', 'id-b'); $urls['id-a']->privateUrl; // https://files.example.com/p/id-a $urls['id-a']->publicUrl; // https://files.example.com/o/my-app/id-a
generateSectionUrls(string $section, string ...$ids): array<string, S4wUrls>
Same, but scoped to a section.
$urls = S4w::generateSectionUrls('documents', 'id-a'); $urls['id-a']->privateUrl; // …/p/documents/id-a $urls['id-a']->publicUrl; // …/o/my-app/documents/id-a
streamPrivate(string $id, string $section = ''): never
Transparently proxies a private S4W file to the current HTTP client.
It fetches /p/[section/]{id} server-to-server under the instance token and
streams the body in chunks straight to php://output — no memory buffering, no
temp file. It forwards the upstream status (200 / 206 / 304), a safe
whitelist of headers, and the client's Range header (video seek / resumable
downloads).
S4w::streamPrivate('692320e4-…', 'documents');
⚠️ Important — this method writes the response and calls
exit.
- Call it only after you have authorized the current user for this file.
- The token stays server-side; it is never exposed to the browser.
- Designed for PHP-FPM. It will not work under Swoole (no
echo/exitoutput model).- On upstream failure it emits a generic
404(not found) or502(upstream unavailable) JSON body — no upstream details leak.
Streaming a private file (full example)
A controller route that checks ownership, then hands off the byte-stream:
#[GetMapping('/files/{id}')] public function download(string $id): never { $file = $this->repo->findUserFile(auth()->id(), $id) ?? throw new NotFoundException(); // your own authorization // From here on the response belongs to S4W. This call exits. S4w::streamPrivate($file->s4wId, $file->section); }
Because Range is forwarded, an HTML <video controls src="/files/{id}"> will
seek correctly, and browsers can resume interrupted downloads.
Data objects
S4wFile (readonly)
| Property | Type | Description |
|---|---|---|
id |
string |
File id (UUID). |
name |
string |
Stored file name. |
section |
?string |
Section, or null. |
mime |
string |
MIME type. |
size |
string |
Size (as reported by S4W). |
extension |
string |
File extension. |
hash |
string |
Content hash (used for deduplication). |
deduplicated |
bool |
Whether the content was deduplicated on store. |
isPublic |
bool |
Whether the file is publicly accessible. |
createdAt |
string |
Creation timestamp. |
privateUrl |
?string |
Private URL, if provided by the API. |
publicUrl |
?string |
Public URL, if provided by the API. |
Helper: $file->isImage() → true for image/jpeg, image/png, image/gif,
image/webp.
S4wUrls (readonly)
| Property | Type |
|---|---|
privateUrl |
?string |
publicUrl |
?string |
PResult<TItem> / PMeta
PResult is { meta: PMeta, data: TItem[] } and is JsonSerializable.
PMeta fields: current, size, total, pages, previous (?int),
next (?int).
Error handling
API errors (non-2xx from S4W) raise
Flytachi\Winter\Cast\Exception\RequestException, carrying the response and the
extracted error message. Input validation (fileUpload) raises
InvalidArgumentException. streamPrivate() does not throw — it terminates
the request with the appropriate HTTP status.
use Flytachi\Winter\Cast\Exception\RequestException; try { $file = S4w::fileGet($id); } catch (RequestException $e) { // $e->getMessage(), inspect the response, etc. }
License
MIT — Flytachi
统计信息
- 总下载量: 1
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 2
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-06-22