定制 pinoox/pinion 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

pinoox/pinion

Composer 安装命令:

composer require pinoox/pinion

包简介

Pinion — resumable chunked upload protocol for PHP (Pinoox, Laravel, any framework)

README 文档

README

Pinion (pinoox/pinion) lets users upload large files in small parts — even when PHP upload_max_filesize / post_max_size are low.

Protocol id: pinion · protocol version: 2 · PHP 8.1+

Layer Registry Package Release
Server (PHP) Packagist pinoox/pinion 1.1.0
Browser (JS) npm @pinooxhq/pinion-client 1.2.0

Protocol version (2) is the wire format (protocol_version in API responses). Release columns are separate semver for the PHP and npm packages.

Quick start

1 — Install

composer require pinoox/pinion          # PHP server (current: 1.0.2)
npm install @pinooxhq/pinion-client       # browser (current: 1.2.0, fetch — no Axios required)

2 — Server: three HTTP steps

use Pinoox\Pinion\Pinion;

Pinion::configure(['storage_path' => '/tmp/pinion']);

$handler = Pinion::http(['destination' => 'uploads/videos']);

$handler->init($_POST);           // POST …/init
$handler->upload($_POST, $file);  // POST …/upload  (one chunk)
$handler->complete($_POST);       // POST …/complete

3 — Browser: one function

import { uploadFile } from '@pinooxhq/pinion-client';

await uploadFile(file, {
  baseURL: '/api/v1/upload',   // prefix — see [Routing](#routing--baseurl)
  unwrapPreset: 'pinoox',
  onProgress: ({ percent }) => console.log(percent + '%'),
});

That is the whole idea: init → upload parts → complete. The client does the loop for you.

Table of contents

What is Pinion

The problem

On shared hosting or default PHP configs you often hit limits like:

upload_max_filesize = 20M
post_max_size = 20M

A 500 MB video cannot be sent in one multipart/form-data request. Pinion splits the file into parts (default 5 MB), uploads them one by one, and assembles the final file on disk.

What Pinion is — and is not

Pinion is Pinion is not
A resumable chunked upload protocol Object storage (S3, MinIO)
A PHP library + JS client A CDN or media pipeline
A stable HTTP contract (pinion v2) A single /upload one-shot endpoint

When to use it

Scenario Why Pinion
Shared hosting (20 MB cap) Send 5 MB parts instead of one huge POST
Slow or mobile networks Resume after disconnect via fingerprint
Video / archive uploads Hundreds of MB or GB without raising php.ini limits
Admin panels & CMS Progress bar with parallel parts
API file intake Same contract across PHP stacks
Integrity-sensitive files SHA-256 per part (chunk_hash) + optional whole-file hash

How it works

sequenceDiagram
    participant Browser
    participant Server
    participant Disk

    Browser->>Server: POST /init (filename, size, fingerprint)
    Server->>Disk: create session + temp workspace
    Server-->>Browser: upload_id, chunk_size, missing_indexes

    loop Each missing part
        Browser->>Browser: slice file, SHA-256 chunk_hash
        Browser->>Server: POST /upload (upload_id, index, chunk)
        Server->>Disk: store part
        Server-->>Browser: ok
    end

    Browser->>Server: POST /complete (upload_id)
    Server->>Disk: assemble → final path
    Server-->>Browser: path / result
Loading

Key concepts:

Concept Role
upload_id Server-side session UUID
fingerprint Client key name:size:lastModified:type — resume same file
chunk_size Bytes per part (negotiated at init)
missing_indexes Which parts still need uploading
chunk_hash SHA-256 of each part — verified if verify_chunks is on
destination Logical folder on server (uploads/videos)

Install

PHP — Packagist

Current release: 1.1.0 (pinoox/pinion)

composer require pinoox/pinion

Pinoox monorepo (local path):

{
  "repositories": [{"type": "path", "url": "packages/pinion"}],
  "require": {"pinoox/pinion": "@dev"}
}

JavaScript — npm

Current release: 1.2.0 (@pinooxhq/pinion-client)

npm install @pinooxhq/pinion-client
# optional, for per-chunk Axios progress:
npm install axios

Without npm (vendor copy)

import { createPinionClient } from './vendor/pinoox/pinion/client/src/index.js';

Level 1 — Simple

Browser: upload one file

The fastest API — no setup object, no Axios:

import { uploadFile } from '@pinooxhq/pinion-client';

const file = input.files[0];

const result = await uploadFile(file, {
  baseURL: '/api/v1/upload',
  unwrapPreset: 'pinoox',
  onProgress: ({ percent }) => {
    progressBar.style.width = percent + '%';
  },
});

console.log('Done:', result);

unwrapPreset: 'pinoox' unwraps responses shaped like { data: { … } } (Pinoox / Laravel envelope). Use 'flat' if your API returns the payload directly.

Server: wire HTTP routes

Pinion needs five endpoints under one prefix. You choose the prefix; the client appends /init, /upload, etc.

Example prefix: /api/v1/upload

Method Route Handler
POST /api/v1/upload/init $handler->init($input)
POST /api/v1/upload/upload $handler->upload($input, $chunkFile)
POST /api/v1/upload/complete $handler->complete($input)
GET /api/v1/upload/status/{id} $handler->status($id)
POST /api/v1/upload/abort/{id} $handler->abort($id)

Minimal PHP router (no framework):

<?php
use Pinoox\Pinion\Pinion;

require 'vendor/autoload.php';

Pinion::configure(['storage_path' => __DIR__ . '/storage/pinion-temp']);

$handler = Pinion::http(['destination' => 'uploads/inbox']);

$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];

$response = match (true) {
    $method === 'POST' && str_ends_with($uri, '/init')
        => $handler->init($_POST),

    $method === 'POST' && str_ends_with($uri, '/upload')
        => $handler->upload($_POST, $_FILES['chunk'] ?? null),

    $method === 'POST' && str_ends_with($uri, '/complete')
        => $handler->complete($_POST),

    $method === 'GET' && preg_match('#/status/([a-f0-9-]+)$#', $uri, $m)
        => $handler->status($m[1]),

    $method === 'POST' && preg_match('#/abort/([a-f0-9-]+)$#', $uri, $m)
        => $handler->abort($m[1]),

    default => ['success' => false, 'error' => ['code' => 'PINION_UNKNOWN', 'message' => 'not_found']],
};

header('Content-Type: application/json');
echo json_encode($response);

Return JSON from HttpHandler as-is, or map success / data / error to your framework response helper.

Routing & baseURL

baseURL in the JS client is the prefix only — not a single upload URL.

Your routes baseURL in client
/api/v1/upload/init, /upload, … '/api/v1/upload'
/api/pinion/init, … '/api/pinion'
/app/pinion/init, … (Pinoox manager) '/app/pinion'

The client calls:

POST {baseURL}/init
POST {baseURL}/upload
POST {baseURL}/complete
GET  {baseURL}/status/{upload_id}
POST {baseURL}/abort/{upload_id}

If you only have one endpoint POST /api/v1/upload for a regular single-file upload, that is not Pinion — use FormData + fetch instead.

Level 2 — Practical patterns

Reusable uploader

Create one client, upload many files:

import { pinion } from '@pinooxhq/pinion-client';

const uploader = pinion({
  baseURL: '/api/v1/upload',
  unwrapPreset: 'pinoox',
  headers: { Authorization: 'Bearer ' + token },
  destination: 'uploads/media',
  extensions: ['mp4', 'zip', 'pdf'],
});

// one file
await uploader.for(file).upload();

// check before uploading
if (!uploader.for(smallFile).needsPinion()) {
  await regularUpload(smallFile);
} else {
  await uploader.for(largeFile).upload();
}

Progress, retry, parallel

await uploader.for(file).upload({
  parallel: 2,          // upload 2 parts at once
  retry: 2,             // retry failed parts
  retryDelayMs: 800,
  onProgress: ({ percent, bytesUploaded, bytesTotal, speed, eta }) => {
    console.log(`${percent}% · ${speed} B/s · ETA ${eta}s`);
  },
  onChunkStart: (index) => console.log('starting part', index),
  onChunkComplete: (index) => console.log('done part', index),
  onError: (err, index) => console.warn(err.code, index),
});

With Axios (optional) you also get raw per-chunk upload events:

import axios from 'axios';
import { pinion } from '@pinooxhq/pinion-client';

const uploader = pinion(axios, { baseURL: '/api/v1/upload', unwrapPreset: 'pinoox' });

await uploader.for(file).upload({
  onUploadProgress: (event, index) => console.log('part', index, event.loaded),
});

Small files vs large files

Use auto to skip Pinion for files below a threshold (default 8 MB):

const result = await uploadFile(file, {
  baseURL: '/api/v1/upload',
  auto: true,
  threshold: 8 * 1024 * 1024,
});

if (result === null) {
  // small file — use your normal single POST
  const form = new FormData();
  form.append('file', file);
  await fetch('/api/v1/upload/simple', { method: 'POST', body: form });
}

Auth headers & CORS

Pass headers once on the client — they apply to every step (init, upload, complete):

const uploader = pinion({
  baseURL: '/api/v1/upload',
  unwrapPreset: 'pinoox',
  headers: {
    Authorization: 'Bearer ' + token,
    'X-App-Id': 'portal',
  },
});

On the server, ensure CORS allows POST + GET on all five routes if the frontend is on another origin.

Cancel an upload

const controller = new AbortController();
uploader.for(file).upload({ signal: controller.signal });
controller.abort();

// or
uploader.cancel();

Batch upload

import { createPinionFetch } from '@pinooxhq/pinion-client';

const client = createPinionFetch({ baseURL: '/api/v1/upload', unwrapPreset: 'pinoox' });

await client.uploadMany([file1, file2, file3], {
  fileParallel: 1,
  onFileStart: (f, i) => console.log('file', i, f.name),
  onFileComplete: (f, result, i) => console.log('done', i, result),
});

Level 3 — Framework integration

Plain PHP

HTTP handler — recommended for web apps:

use Pinoox\Pinion\Pinion;
use Pinoox\Pinion\Support\NativePathResolver;

Pinion::configure([
    'storage_path' => '/var/www/storage/pinion-temp',
    'chunk_size' => 5 * 1024 * 1024,
    'verify_chunks' => true,
], new NativePathResolver('/var/www'));

$handler = Pinion::http([
    'destination' => 'uploads/documents',
    'extensions' => ['pdf', 'docx'],
]);

Fluent builder — for scripts and jobs:

$result = Pinion::begin()
    ->filename('report-Q1.pdf')
    ->size(48_000_000)
    ->to('uploads/documents')
    ->extensions(['pdf'])
    ->fingerprint($clientFingerprint)
    ->chunkSize('5MB')
    ->init();

if (!$result->success) {
    exit($result->error);
}

$uploadId = $result->session->id;

foreach ($result->session->missingIndexes() as $index) {
    Pinion::manager()->receive($uploadId, $index, $chunkBinary, $chunkHash);
}

$complete = Pinion::manager()->complete($uploadId);
// $complete->path → absolute path on disk

Pinoox (HMVC)

Inside a Pinoox project use Pinoox\Portal\Pinion — config, temp storage, Flysystem/S3, and pinx_file integration are wired in pincore.

Piece Path Role
Portal Pinoox\Portal\Pinion App-facing API
Bridge pincore/Component/Pinion/ ProtocolManager, StorageCompletion, HttpHandler
Config pincore/config/pinion.config.php Chunk TTL, PINION_MODE, storage defaults
Trait PinionUploadActions Drop-in controller actions per app
CLI php pinoox pinion:list Ops on temp sessions

Storage modes (pinion.config.phpdefaults.mode):

Mode Behaviour
auto Use Flysystem (Portal\File) when app filesystem.disk is not local (e.g. S3)
storage Always publish through managed file storage + optional FileModel row
local Assemble to project path via path() (legacy / manual folders)

Chunks always stage under storage/pinion. On complete, managed mode streams the assembled file to the app disk (local or S3) via UploadBuilder.

App controller (HMVC) — use the trait:

use Pinoox\Component\Kernel\Controller\ApiController;
use Pinoox\Component\Pinion\Concerns\PinionUploadActions;

class PinionController extends ApiController
{
    use PinionUploadActions;

    protected function pinionDefaults(): array
    {
        return [
            'destination' => 'uploads/media',
            'extensions' => ['mp4', 'mov', 'webm'],
            'mode' => 'auto',       // S3 when app disk is s3
            'record' => true,       // create pinx_file row
            'access' => 'public',
            'group' => 'media',
        ];
    }
}

Local-only upload (no Flysystem — e.g. manager .pinx packages):

protected function pinionDefaults(): array
{
    return [
        'destination' => 'downloads/packages/manual',
        'extensions' => ['pinx'],
        'mode' => 'local',
        'storage' => false,
        'record' => false,
    ];
}

Browser client — npm @pinooxhq/pinion-client:

import { uploadFile } from '@pinooxhq/pinion-client';

await uploadFile(file, {
  baseURL: '/app/pinion',
  unwrapPreset: 'pinoox',
  destination: 'uploads/media',
  meta: { group: 'media' },
  onProgress: ({ percent }) => console.log(percent),
});

On complete with managed storage, the API returns file_id, url, and thumb (when applicable).

Routes (per app under apps/{package}/routes/):

POST /app/pinion/init
POST /app/pinion/upload
POST /app/pinion/complete
GET  /app/pinion/status/{uploadId}
POST /app/pinion/abort/{uploadId}

→ client baseURL: '/app/pinion'

Template: pincore/config/pinion.routes.template.php

Config: pincore/config/pinion.config.php · temp: storage/pinion (pinion_uploads)

CLI:

php pinoox pinion:list
php pinoox pinion:info {upload_id}
php pinoox pinion:clean --abort={upload_id}

Laravel

Laravel 10+ — auto-discovery via PinionServiceProvider.

1. Publish config

php artisan vendor:publish --tag=pinion-config

2. Routes (routes/api.php)

use App\Http\Controllers\PinionUploadController;

Route::prefix('api/v1/upload')->group(function () {
    Route::post('init', [PinionUploadController::class, 'init']);
    Route::post('upload', [PinionUploadController::class, 'upload']);
    Route::post('complete', [PinionUploadController::class, 'complete']);
    Route::get('status/{uploadId}', [PinionUploadController::class, 'status']);
    Route::post('abort/{uploadId}', [PinionUploadController::class, 'abort']);
});

3. Controller

namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Pinoox\Pinion\HttpHandler;

final class PinionUploadController extends Controller
{
    public function __construct(private readonly HttpHandler $pinion) {}

    public function init(Request $request): JsonResponse
    {
        return $this->json($this->pinion->init($request->all()));
    }

    public function upload(Request $request): JsonResponse
    {
        return $this->json($this->pinion->upload($request->all(), $request->file('chunk')));
    }

    public function complete(Request $request): JsonResponse
    {
        return $this->json($this->pinion->complete($request->all()));
    }

    public function status(string $uploadId): JsonResponse
    {
        return $this->json($this->pinion->status($uploadId));
    }

    public function abort(string $uploadId): JsonResponse
    {
        return $this->json($this->pinion->abort($uploadId));
    }

    private function json(array $payload): JsonResponse
    {
        $status = (int) ($payload['status'] ?? ($payload['success'] ? 200 : 400));

        return response()->json(
            $payload['success'] ? ($payload['data'] ?? null) : ($payload['error'] ?? $payload),
            $status,
        );
    }
}

4. Facade (server-side jobs)

use Pinoox\Pinion\Laravel\Facades\Pinion;

$result = Pinion::begin()
    ->filename('dataset-export.csv')
    ->size(filesize($path))
    ->to('exports')
    ->init();

Level 4 — Advanced

Programmatic API (no HTTP)

Use Manager directly when chunks arrive from CLI, queue workers, or non-HTTP sources:

$manager = Pinion::manager();

$result = $manager->init(
    filename: 'archive.zip',
    size: 1_073_741_824,
    destination: 'uploads/archives',
    extensions: ['zip'],
    fingerprint: $clientFingerprint,
);

$session = $result->session;
$manager->receive($session->id, 0, $binary, $chunkHash);
$manager->receive($session->id, 1, $binary, $chunkHash);
// …

$done = $manager->complete($session->id);
echo $done->path;

Low-level browser control (manual steps):

import { createPinionFetch, sha256Hex } from '@pinooxhq/pinion-client';

const client = createPinionFetch({ baseURL: '/api/v1/upload', unwrapPreset: 'pinoox' });

const session = await client.api.init({
  filename: file.name,
  size: file.size,
  fingerprint: client.buildFingerprint(file),
});

const blob = file.slice(0, session.chunk_size);
const form = new FormData();
form.append('upload_id', session.id);
form.append('index', '0');
form.append('chunk_hash', await sha256Hex(blob));
form.append('chunk', blob);

await client.api.uploadPart(form);
await client.api.complete(session.id);

Resume & fingerprint

The client builds a fingerprint from file metadata:

{name}:{size}:{lastModified}:{type}

On init, if the same fingerprint already has a pending session, the server returns it with resumed: true and missing_indexes — only missing parts are uploaded.

The JS client caches upload_id in localStorage (key: pinion_sessions) until complete succeeds.

Force resume explicitly:

await uploader.for(file).resume({ parallel: 2 });
// same as upload() — both run the full init → parts → complete flow

Check stored session:

const fp = uploader.buildFingerprint(file);
const cached = uploader.getStoredSession(fp);

Checksums & integrity

Field When Purpose
chunk_hash Each POST /upload SHA-256 of that part — verified when verify_chunks: true
file_hash POST /init or /complete Optional whole-file hash — verified when verify_file_hash: true

Client sends chunk_hash automatically. To send whole-file hash on complete:

import { sha256Hex } from '@pinooxhq/pinion-client';

const fileHash = await sha256Hex(file);

await uploader.for(file).upload({ fileHash });

Configuration reference

Pass to Pinion::configure() or copy config/pinion.php.

Key Default Description
protocol pinion Protocol identifier (read-only in responses)
protocol_version 2 Protocol version
chunk_size 5242880 (5 MB) Default part size
min_chunk_size 1048576 (1 MB) Lower clamp
max_chunk_size 10485760 (10 MB) Upper clamp
ttl 86400 Session lifetime (seconds)
max_file_size 2147483648 (2 GB) Max declared file size
storage_path /tmp/pinion Temp workspace for in-progress uploads
storage_strategy parts parts or sparse
verify_chunks true Require matching chunk_hash
verify_file_hash false Require file_hash on complete

Laravel .env:

PINION_CHUNK_SIZE=5242880
PINION_TTL=86400
PINION_MAX_FILE=2147483648
PINION_PATH=/var/www/storage/pinion-temp
PINION_STRATEGY=parts
PINION_VERIFY_CHUNKS=true
PINION_VERIFY_FILE=false

Per-request defaults via HttpHandler:

$handler = Pinion::http([
    'destination' => 'uploads/videos',
    'extensions' => ['mp4', 'mov'],
]);
// client can override destination per init, or use these defaults

Storage strategies

Strategy On disk Best for
parts {id}/parts/0.part, 1.part, … Parallel client uploads (default)
sparse Single {id}/blob.part with offset writes Fewer files, sequential writes

Temp files live under storage_path. On complete, the assembled file moves to the resolved destination. Expired pending sessions are cleaned via cleanExpired().

Custom destinations

By default NativePathResolver maps to('uploads/videos') relative to a base path. For multi-root apps implement PathResolverInterface:

use Pinoox\Pinion\Contract\PathResolverInterface;

final class AppPathResolver implements PathResolverInterface
{
    public function resolve(string $reference): string
    {
        return match ($reference) {
            'videos' => '/data/media/videos',
            'documents' => '/data/media/docs',
            default => '/data/media/' . ltrim($reference, '/'),
        };
    }
}

Pinion::configure($config, new AppPathResolver());

Maintenance & CLI

$session = Pinion::manager()->status($uploadId);
// → progress, missing_indexes, bytes_received

$pending = Pinion::manager()->list('pending');
$removed = Pinion::manager()->cleanExpired();
Pinion::manager()->abort($uploadId);

Pinoox CLI:

php pinoox pinion:list
php pinoox pinion:info a1b2c3d4-...
php pinoox pinion:clean
php pinoox pinion:clean --abort=a1b2c3d4-...

HTTP protocol (full reference)

Endpoints

Step Method Path Body
Init / resume POST {prefix}/init JSON
Upload part POST {prefix}/upload multipart/form-data
Complete POST {prefix}/complete JSON
Status GET {prefix}/status/{upload_id}
Abort POST {prefix}/abort/{upload_id} JSON (optional)

Init request

{
  "filename": "course-video.mp4",
  "size": 524288000,
  "destination": "uploads/videos",
  "fingerprint": "video.mp4:524288000:1718640000000:video/mp4",
  "chunk_size": 5242880,
  "mime": "video/mp4",
  "extensions": ["mp4", "mov"],
  "file_hash": null,
  "meta": { "user_id": 42 }
}

Init response

{
  "id": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
  "filename": "course-video.mp4",
  "size": 524288000,
  "chunk_size": 5242880,
  "total_chunks": 100,
  "bytes_received": 0,
  "missing_indexes": [0, 1, 2, 3],
  "protocol": "pinion",
  "protocol_version": 2,
  "resumable": true,
  "resumed": false
}

Same fingerprint on a pending session → resumed: true, missing_indexes only lists gaps.

Upload part request

multipart/form-data fields:

Field Required Notes
upload_id yes Session UUID
index yes Zero-based part index
chunk yes Binary file field
chunk_hash recommended SHA-256 hex of chunk

Do not set Content-Type: multipart/form-data manually — let the client / browser set the boundary.

Complete request

{
  "upload_id": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
  "file_hash": "optional-sha256-of-whole-file"
}

Error envelope

HttpHandler returns:

{
  "success": false,
  "status": 400,
  "error": {
    "code": "PINION_INVALID",
    "message": "invalid_chunk_request",
    "details": {}
  }
}

Common codes: PINION_INIT_FAILED, PINION_INVALID, PINION_CHUNK_HASH_MISMATCH, PINION_SESSION_EXPIRED, PINION_FILE_TOO_LARGE.

Browser client (full reference)

Published as @pinooxhq/pinion-client 1.2.0. Detailed npm guide: client/README.md

Usage levels

Level API When
1 — Fastest uploadFile(file, options) Single button
2 — Fluent pinion({ baseURL }).for(file).upload() Reusable instance
3 — Full createPinionFetch(options) Batch, hooks, cancel
4 — Manual client.api.init() / uploadPart() / complete() Custom flow
5 — Axios pinion(axios, options) Per-chunk onUploadProgress
6 — Custom createPinionClient({ transport }) Own HTTP layer

Transport

Mode How client.transport.kind
fetch (default) pinion({ baseURL }) 'fetch'
Axios pinion(axios, { baseURL }) 'axios'

Unwrap presets

Preset Unwraps to
pinoox response.data.data
laravel same envelope
flat response.data
raw full response object

Set unwrapPreset: 'pinoox' when your PHP API wraps data in { data: … }.

Key exports

Export Role
uploadFile(file, options) One-shot upload
pinion(options) Fluent factory
createPinionFetch(options) Explicit fetch client
createPinionAxios(axios, options) { axios, client }
client.upload(file, opts) Full flow with parallel + retry
client.uploadMany(files, opts) Batch
client.api Low-level HTTP steps
buildFingerprint(file) Resume key
shouldUsePinion(file, threshold?) Skip Pinion for small files
sha256Hex(blob) Part checksum
PinionError Typed error with .code

PHP API surface

Class / method Role
Pinion::configure($config, $pathResolver?) Boot once
Pinion::manager() Manager singleton
Pinion::begin() Fluent Builderinit()
Pinion::http($defaults) HttpHandler for HTTP apps
Manager::init(...) Create or resume session
Manager::receive(...) Store one part
Manager::complete(...) Assemble final file
Manager::status(...) Progress + missing_indexes
Manager::abort(...) Cancel session
Manager::list($status) List sessions
Manager::cleanExpired() Purge expired pending
HttpHandler HTTP → { success, data?, error? }
Builder filename(), size(), to(), extensions(), fingerprint(), chunkSize(), init()
Session missingIndexes(), progress()
Result success, session, path, error, resumed
PathResolverInterface Map logical destination → absolute path

Pinoox (pincore): Pinoox\Portal\Pinion, Pinoox\Component\Pinion\HttpHandler, CLI commands.

Laravel: PinionServiceProvider, Pinion facade.

Troubleshooting

Symptom Likely cause Fix
PINION_INIT_FAILED in browser Wrong unwrapPreset Try pinoox, flat, or custom unwrap
404 on /upload Wrong baseURL baseURL = prefix only, not full file URL
Multipart rejected Manual Content-Type Let client set boundary on FormData
Upload stuck at N% Missing parts client.api.status(uploadId)missing_indexes
chunk_hash mismatch Corrupt part or wrong hash Client computes SHA-256 per slice — don't transform binary
Session expired ttl exceeded Re-init; same fingerprint may start fresh
Small file uses Pinion unnecessarily No auto threshold uploadFile(file, { auto: true })
CORS error Preflight blocked Allow POST/GET on all five routes + credentials if needed
PINION_NO_FETCH (Node) No global fetch Node 18+ or pass options.fetch

Package structure

packages/pinion/
├── composer.json           # pinoox/pinion 1.1.0 (Packagist)
├── client/                 # @pinooxhq/pinion-client 1.2.0 (npm)
│   ├── src/                # createClient, transport, checksum, …
│   ├── types/              # TypeScript definitions
│   ├── package.json
│   └── README.md           # npm client guide (publish, API)
├── config/pinion.php       # default PHP config
├── src/                    # PHP protocol engine
│   ├── Pinion.php          # entry point
│   ├── Manager.php         # core engine
│   ├── HttpHandler.php     # HTTP adapter
│   ├── Builder.php         # fluent init
│   ├── Session.php / Result.php
│   ├── Laravel/            # service provider + facade
│   └── Support/            # NativePathResolver, …
├── tests/
└── README.md               # this file

License

MIT — Pinoox

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 6
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-06-17

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固