dockerfile

This commit is contained in:
浪子 2026-03-19 19:39:14 +08:00
parent 04eccc850d
commit 62cffc6f5f
13 changed files with 578 additions and 51 deletions

17
.dockerignore Normal file
View File

@ -0,0 +1,17 @@
.git
.gitignore
.env
.env.*
!.env.example
node_modules
vendor
public/build
public/hot
storage/logs/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
bootstrap/cache/*.php
database/database.sqlite
npm-debug.log*
yarn-error.log*

63
Dockerfile Normal file
View File

@ -0,0 +1,63 @@
# syntax=docker/dockerfile:1.7
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
--no-dev \
--prefer-dist \
--no-interaction \
--no-progress \
--no-scripts
COPY . .
RUN composer dump-autoload --optimize --no-dev \
&& php artisan package:discover --ansi
FROM node:20-alpine AS frontend
WORKDIR /app
COPY --from=vendor /app /app
RUN npm ci && npm run build
FROM php:8.2-apache AS runtime
ENV APACHE_DOCUMENT_ROOT=/var/www/html/public
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libonig-dev \
libsqlite3-dev \
libzip-dev \
unzip \
&& docker-php-ext-install \
mbstring \
opcache \
pdo_mysql \
pdo_sqlite \
zip \
&& a2enmod expires headers rewrite \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /var/www/html
COPY --from=vendor /app /var/www/html
COPY --from=frontend /app/public/build /var/www/html/public/build
COPY docker/apache/000-default.conf /etc/apache2/sites-available/000-default.conf
COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
COPY docker/entrypoint.sh /usr/local/bin/app-entrypoint
RUN chmod +x /usr/local/bin/app-entrypoint \
&& mkdir -p storage/framework/cache/data storage/framework/sessions storage/framework/views bootstrap/cache \
&& chown -R www-data:www-data storage bootstrap/cache
EXPOSE 80
ENTRYPOINT ["app-entrypoint"]
CMD ["apache2-foreground"]

View File

@ -43,6 +43,101 @@ composer run setup
composer run dev
```
## Docker
仓库已提供通用容器化文件:
- `Dockerfile`
- `.dockerignore`
- `docker/apache/000-default.conf`
- `docker/entrypoint.sh`
- `docker/php/opcache.ini`
- `docker-compose.example.yml`
### 1. 构建镜像
```bash
docker build -t tstore:latest .
```
这个镜像是多阶段构建,包含:
- Composer 依赖安装
- Vite 前端资源构建
- Apache + PHP 8.2 运行时
容器内网站根目录已经指向 `public/`,不需要再额外改 Web 根目录。
### 2. 单容器运行
更适合已经有外部 MySQL 的场景。
```bash
docker run -d \
--name tstore \
-p 8080:80 \
--env-file .env \
-e APP_ENV=production \
-e APP_DEBUG=false \
-e APP_URL=http://localhost:8080 \
-e LOG_CHANNEL=stderr \
-e DB_CONNECTION=mysql \
-e DB_HOST=your-db-host \
-e DB_PORT=3306 \
-e DB_DATABASE=tstore \
-e DB_USERNAME=tstore \
-e DB_PASSWORD=your-password \
-e SESSION_DRIVER=file \
-e CACHE_STORE=file \
-e QUEUE_CONNECTION=sync \
-e RUN_MIGRATIONS=1 \
-e RUN_OPTIMIZE=1 \
tstore:latest
```
如果你希望容器启动时自动创建后台管理员账号,可以再加:
```bash
-e RUN_ADMIN_SEEDER=1
```
说明:
- `RUN_MIGRATIONS=1` 会在容器启动时执行 `php artisan migrate --force`
- `RUN_ADMIN_SEEDER=1` 会执行 `AdminUserSeeder`
- `RUN_OPTIMIZE=1` 会执行 `php artisan optimize`
### 3. 使用 Docker Compose
仓库提供了一个通用示例文件:
```bash
cp docker-compose.example.yml docker-compose.yml
docker compose up -d --build
```
默认暴露端口:
- 应用:`8080`
- MySQL不对宿主机暴露仅供容器内部访问
首次启动前,建议先修改:
- `docker-compose.yml` 中的数据库密码
- `.env` 中的 `APP_URL`
- `.env` 中的 `SESSION_DRIVER`、`CACHE_STORE`、`QUEUE_CONNECTION`
- `.env` 中的 `STORE_ADMIN_TOKEN`
- `.env` 中的 `STORE_PLUGIN_ACCESS_TOKEN`
- `.env` 中的 `ADMIN_EMAIL`
- `.env` 中的 `ADMIN_PASSWORD`
### 4. 容器化运行时建议
- 生产环境优先使用 MySQL不建议继续使用 SQLite
- 如果要持久化上传的 ZIP 包和日志,保留 `storage` 卷挂载
- 不要把真实 `.env` 打进镜像,运行时通过 `--env-file` 或 Compose 注入
- 如果上线到生产,建议把 `APP_URL` 改成正式域名,并在网关层处理 HTTPS
## 生产部署
以下步骤以 `Linux + Nginx + PHP-FPM` 为例。生产环境不要运行 `php artisan serve``npm run dev`

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers\WebAdmin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\PublishZipVersionRequest;
use App\Http\Requests\Admin\StoreCategoryRequest;
use App\Http\Requests\Admin\StorePackageRequest;
use App\Models\Category;
@ -220,17 +221,10 @@ class DashboardController extends Controller
return redirect()->route('webadmin.packages.show', [$type, $slug])->with('success', '版本已添加v' . $version->version);
}
public function publishVersion(Request $request, string $type, string $slug): RedirectResponse
public function publishVersion(PublishZipVersionRequest $request, string $type, string $slug): RedirectResponse
{
$package = $this->findPackage($type, $slug);
$validated = $request->validate([
'package_file' => ['required', 'file', 'mimes:zip', 'max:51200'],
'is_stable' => ['nullable', 'boolean'],
'mark_as_latest' => ['nullable', 'boolean'],
'changelog' => ['nullable', 'string'],
'published_at' => ['nullable', 'date'],
]);
$validated = $request->validated();
try {
$version = $this->publishService->publishFromZip($package, $request->file('package_file'), $validated);

View File

@ -3,6 +3,8 @@
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
use ZipArchive;
class PublishZipVersionRequest extends FormRequest
{
@ -11,14 +13,75 @@ class PublishZipVersionRequest extends FormRequest
return true;
}
protected function prepareForValidation(): void
{
$extensions = $this->input('php_extensions');
if (is_string($extensions)) {
$this->merge([
'php_extensions' => collect(preg_split('/[,\s]+/u', $extensions))
->map(fn ($item) => trim((string) $item))
->filter()
->values()
->all(),
]);
}
}
public function rules(): array
{
return [
'package_file' => ['required', 'file', 'mimes:zip', 'max:51200'],
'version' => ['nullable', 'string', 'max:32', 'regex:/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/'],
'typecho_min' => ['nullable', 'string', 'max:16'],
'typecho_max' => ['nullable', 'string', 'max:16'],
'php_min' => ['nullable', 'string', 'max:16'],
'php_max' => ['nullable', 'string', 'max:16'],
'php_extensions' => ['nullable', 'array'],
'php_extensions.*' => ['string', 'max:32'],
'is_stable' => ['nullable', 'boolean'],
'mark_as_latest' => ['nullable', 'boolean'],
'changelog' => ['nullable', 'string'],
'published_at' => ['nullable', 'date'],
];
}
public function withValidator(Validator $validator): void
{
$validator->after(function (Validator $validator) {
$file = $this->file('package_file');
if (!$file || !$file->isValid() || $this->zipHasManifestAtRoot($file->getRealPath())) {
return;
}
if (blank((string) $this->input('version', ''))) {
$validator->errors()->add('version', 'zip 根目录没有 manifest.json 时,必须手动填写版本号。');
}
});
}
private function zipHasManifestAtRoot(string $path): bool
{
$zip = new ZipArchive();
if ($zip->open($path) !== true) {
return false;
}
try {
for ($i = 0; $i < $zip->numFiles; $i++) {
$stat = $zip->statIndex($i);
$name = trim((string) ($stat['name'] ?? ''), '/');
if ($name === 'manifest.json') {
return true;
}
}
return false;
} finally {
$zip->close();
}
}
}

View File

@ -32,8 +32,8 @@ class VersionPublishService
$sha256 = hash_file('sha256', $incomingPath);
$size = filesize($incomingPath) ?: 0;
$manifest = $this->extractAndValidate($incomingPath, $extractDir, $package, $sha256);
$versionNumber = (string) $manifest['version'];
$metadata = $this->extractAndValidate($incomingPath, $extractDir, $package, $sha256, $options);
$versionNumber = (string) $metadata['version'];
if ($package->versions()->where('version', $versionNumber)->exists()) {
throw new RuntimeException('version already exists');
@ -46,12 +46,12 @@ class VersionPublishService
$payload = [
'version' => $versionNumber,
'changelog' => $options['changelog'] ?? ($manifest['changelog'] ?? ''),
'typecho_min' => data_get($manifest, 'compatibility.typecho_min', '1.2.0'),
'typecho_max' => data_get($manifest, 'compatibility.typecho_max', ''),
'php_min' => data_get($manifest, 'compatibility.php_min', '7.4'),
'php_max' => data_get($manifest, 'compatibility.php_max', ''),
'php_extensions' => data_get($manifest, 'compatibility.php_extensions', []),
'changelog' => $options['changelog'] ?? ($metadata['changelog'] ?? ''),
'typecho_min' => $metadata['typecho_min'] ?? '1.2.0',
'typecho_max' => $metadata['typecho_max'] ?? '',
'php_min' => $metadata['php_min'] ?? '7.4',
'php_max' => $metadata['php_max'] ?? '',
'php_extensions' => $metadata['php_extensions'] ?? [],
'package_url' => $this->buildPackageUrl($package, $versionNumber),
'package_size' => $size,
'sha256' => $sha256,
@ -74,7 +74,7 @@ class VersionPublishService
}
}
private function extractAndValidate(string $zipPath, string $extractDir, Package $package, string $sha256): array
private function extractAndValidate(string $zipPath, string $extractDir, Package $package, string $sha256, array $options): array
{
$zip = new ZipArchive();
$opened = $zip->open($zipPath);
@ -84,7 +84,6 @@ class VersionPublishService
$topLevel = [];
$manifestRaw = null;
$rootDirExists = false;
for ($i = 0; $i < $zip->numFiles; $i++) {
$stat = $zip->statIndex($i);
@ -104,41 +103,42 @@ class VersionPublishService
}
}
if ($manifestRaw === null) {
$zip->close();
throw new RuntimeException('manifest.json not found at zip root');
}
try {
if ($manifestRaw !== null) {
$manifest = json_decode($manifestRaw, true);
if (!is_array($manifest)) {
throw new RuntimeException('invalid manifest.json');
}
$manifest = json_decode($manifestRaw, true);
if (!is_array($manifest)) {
$zip->close();
throw new RuntimeException('invalid manifest.json');
}
$this->validateManifest($manifest, $package, $sha256);
$this->assertTopLevelStructure($topLevel, ['manifest.json', $package->slug], 'zip root structure must contain only manifest.json and package root dir');
$metadata = [
'version' => (string) $manifest['version'],
'changelog' => (string) ($manifest['changelog'] ?? ''),
'typecho_min' => (string) data_get($manifest, 'compatibility.typecho_min', '1.2.0'),
'typecho_max' => (string) data_get($manifest, 'compatibility.typecho_max', ''),
'php_min' => (string) data_get($manifest, 'compatibility.php_min', '7.4'),
'php_max' => (string) data_get($manifest, 'compatibility.php_max', ''),
'php_extensions' => $this->normalizePhpExtensions(data_get($manifest, 'compatibility.php_extensions', [])),
];
} else {
$this->assertTopLevelStructure($topLevel, [$package->slug], 'zip root structure must contain only package root dir when manifest.json is absent');
$metadata = $this->buildFallbackMetadata($options);
}
$this->validateManifest($manifest, $package, $sha256);
$allowedTopLevel = ['manifest.json', $package->slug];
$topLevelNames = array_keys($topLevel);
sort($topLevelNames);
sort($allowedTopLevel);
if ($topLevelNames !== $allowedTopLevel) {
if (!$zip->extractTo($extractDir)) {
throw new RuntimeException('failed to extract zip package');
}
} finally {
$zip->close();
throw new RuntimeException('zip root structure must contain only manifest.json and package root dir');
}
if (!$zip->extractTo($extractDir)) {
$zip->close();
throw new RuntimeException('failed to extract zip package');
}
$zip->close();
$rootDir = $extractDir . DIRECTORY_SEPARATOR . $package->slug;
$rootDirExists = is_dir($rootDir);
if (!$rootDirExists) {
if (!is_dir($rootDir)) {
throw new RuntimeException('package root dir missing after extraction');
}
return $manifest;
return $metadata;
}
private function validateManifest(array $manifest, Package $package, string $sha256): void
@ -212,4 +212,55 @@ class VersionPublishService
return $base === '' ? $path : $base . $path;
}
private function assertTopLevelStructure(array $topLevel, array $allowedTopLevel, string $message): void
{
$topLevelNames = array_keys($topLevel);
sort($topLevelNames);
sort($allowedTopLevel);
if ($topLevelNames !== $allowedTopLevel) {
throw new RuntimeException($message);
}
}
private function buildFallbackMetadata(array $options): array
{
$version = trim((string) ($options['version'] ?? ''));
if ($version === '') {
throw new RuntimeException('manifest.json not found at zip root; version is required for zip publish');
}
if (!preg_match('/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/', $version)) {
throw new RuntimeException('fallback version is not valid semver');
}
return [
'version' => $version,
'changelog' => (string) ($options['changelog'] ?? ''),
'typecho_min' => (string) ($options['typecho_min'] ?? '1.2.0'),
'typecho_max' => (string) ($options['typecho_max'] ?? ''),
'php_min' => (string) ($options['php_min'] ?? '7.4'),
'php_max' => (string) ($options['php_max'] ?? ''),
'php_extensions' => $this->normalizePhpExtensions($options['php_extensions'] ?? []),
];
}
private function normalizePhpExtensions(mixed $value): array
{
if (is_string($value)) {
$value = preg_split('/[,\s]+/u', $value);
}
if (!is_array($value)) {
return [];
}
return collect($value)
->map(fn ($item) => trim((string) $item))
->filter()
->values()
->all();
}
}

View File

@ -0,0 +1,52 @@
services:
app:
build:
context: .
image: tstore:latest
ports:
- "8080:80"
env_file:
- .env
environment:
APP_ENV: production
APP_DEBUG: "false"
APP_URL: http://localhost:8080
LOG_CHANNEL: stderr
DB_CONNECTION: mysql
DB_HOST: mysql
DB_PORT: "3306"
DB_DATABASE: tstore
DB_USERNAME: tstore
DB_PASSWORD: tstore_password
SESSION_DRIVER: file
CACHE_STORE: file
QUEUE_CONNECTION: sync
RUN_MIGRATIONS: "1"
RUN_ADMIN_SEEDER: "0"
RUN_OPTIMIZE: "1"
depends_on:
mysql:
condition: service_healthy
volumes:
- storage_data:/var/www/html/storage
mysql:
image: mysql:8.4
restart: unless-stopped
environment:
MYSQL_DATABASE: tstore
MYSQL_USER: tstore
MYSQL_PASSWORD: tstore_password
MYSQL_ROOT_PASSWORD: root_password
command: --default-authentication-plugin=mysql_native_password
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot_password"]
interval: 10s
timeout: 5s
retries: 10
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
storage_data:

View File

@ -0,0 +1,13 @@
<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html/public
<Directory /var/www/html/public>
AllowOverride All
Options FollowSymLinks
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

19
docker/entrypoint.sh Normal file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env sh
set -eu
mkdir -p storage/framework/cache/data storage/framework/sessions storage/framework/views bootstrap/cache
chown -R www-data:www-data storage bootstrap/cache
if [ "${RUN_MIGRATIONS:-0}" = "1" ]; then
php artisan migrate --force
fi
if [ "${RUN_ADMIN_SEEDER:-0}" = "1" ]; then
php artisan db:seed --class="Database\\Seeders\\AdminUserSeeder" --force
fi
if [ "${RUN_OPTIMIZE:-0}" = "1" ]; then
php artisan optimize
fi
exec "$@"

7
docker/php/opcache.ini Normal file
View File

@ -0,0 +1,7 @@
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=128
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.revalidate_freq=0

View File

@ -115,19 +115,27 @@
<div class="section-title">
<div>
<h2>zip 上传发布</h2>
<p>直接调用 publishFromZip manifest 自动生成版本</p>
<p>优先读取 zip 根目录的 manifest.json如果没有 manifest.json则使用下方手动填写的版本和兼容性字段</p>
</div>
</div>
<form method="post" action="{{ route('webadmin.packages.publish', [$package->type, $package->slug]) }}" class="grid" enctype="multipart/form-data">
@csrf
<div class="field"><label>zip 文件</label><input class="input" type="file" name="package_file" accept=".zip" required></div>
<div class="field"><label>发布说明(可选)</label><textarea name="changelog" placeholder="补充这次发布说明"></textarea></div>
<div class="field"><label>版本号(无 manifest 时必填)</label><input class="input" name="version" value="{{ old('version') }}" placeholder="1.0.0"></div>
<div class="field"><label>发布说明(可选)</label><textarea name="changelog" placeholder="补充这次发布说明">{{ old('changelog') }}</textarea></div>
<div class="form-grid">
<div class="field"><label>Typecho Min</label><input class="input" name="typecho_min" value="{{ old('typecho_min', '1.2.0') }}" placeholder="1.2.0"></div>
<div class="field"><label>Typecho Max</label><input class="input" name="typecho_max" value="{{ old('typecho_max') }}" placeholder="1.3.*"></div>
<div class="field"><label>PHP Min</label><input class="input" name="php_min" value="{{ old('php_min', '7.4') }}" placeholder="7.4"></div>
<div class="field"><label>PHP Max</label><input class="input" name="php_max" value="{{ old('php_max') }}" placeholder="8.3"></div>
<div class="field"><label>PHP Extensions</label><input class="input" name="php_extensions" value="{{ is_array(old('php_extensions')) ? implode(',', old('php_extensions')) : old('php_extensions') }}" placeholder="curl,json"></div>
<div class="field"><label>发布时间(可选)</label><input class="input" type="datetime-local" name="published_at" value="{{ old('published_at') }}"></div>
</div>
<div class="form-grid">
<div class="field"><label>发布时间(可选)</label><input class="input" type="datetime-local" name="published_at"></div>
<div class="field"><label>发布选项</label>
<div class="tags">
<label><input type="checkbox" name="is_stable" value="1" checked> 设为稳定版</label>
<label><input type="checkbox" name="mark_as_latest" value="1" checked> 设为最新</label>
<label><input type="checkbox" name="is_stable" value="1" @checked(old('is_stable', 1))> 设为稳定版</label>
<label><input type="checkbox" name="mark_as_latest" value="1" @checked(old('mark_as_latest', 1))> 设为最新</label>
</div>
</div>
</div>

View File

@ -2,11 +2,13 @@
namespace Tests\Feature;
// use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ExampleTest extends TestCase
{
use RefreshDatabase;
/**
* A basic test example.
*/

View File

@ -0,0 +1,143 @@
<?php
namespace Tests\Feature;
use App\Models\Package;
use App\Services\VersionPublishService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\File;
use RuntimeException;
use Tests\TestCase;
use ZipArchive;
class VersionPublishServiceTest extends TestCase
{
use RefreshDatabase;
/**
* @var array<int, string>
*/
private array $temporaryZipPaths = [];
public function test_it_can_publish_zip_without_manifest_using_manual_metadata(): void
{
$package = Package::query()->create([
'type' => 'plugin',
'slug' => 'DemoPlugin',
'name' => 'Demo Plugin',
'summary' => '',
'description' => '',
'author' => '',
'homepage' => '',
'icon_url' => '',
'license' => '',
'status' => 'draft',
'is_featured' => false,
'sort_order' => 0,
'download_count' => 0,
'latest_version' => '',
]);
$uploadedFile = $this->makeZipUpload($package->slug, [
'Plugin.php' => '<?php echo "ok";',
]);
$version = app(VersionPublishService::class)->publishFromZip($package, $uploadedFile, [
'version' => '1.2.3',
'typecho_min' => '1.2.0',
'typecho_max' => '1.3.*',
'php_min' => '8.0',
'php_max' => '8.3',
'php_extensions' => ['curl', 'json'],
'is_stable' => true,
'mark_as_latest' => true,
]);
$this->assertSame('1.2.3', $version->version);
$this->assertSame('1.2.0', $version->typecho_min);
$this->assertSame('1.3.*', $version->typecho_max);
$this->assertSame('8.0', $version->php_min);
$this->assertSame('8.3', $version->php_max);
$this->assertSame(['curl', 'json'], $version->php_extensions_array);
$this->assertTrue($version->is_stable);
$this->assertTrue($version->is_latest);
$this->assertFileExists(storage_path('app/packages/plugin/DemoPlugin/1.2.3.zip'));
$this->assertSame('1.2.3', $package->fresh()->latest_version);
}
public function test_it_requires_manual_version_when_manifest_is_missing(): void
{
$package = Package::query()->create([
'type' => 'plugin',
'slug' => 'DemoPlugin',
'name' => 'Demo Plugin',
'summary' => '',
'description' => '',
'author' => '',
'homepage' => '',
'icon_url' => '',
'license' => '',
'status' => 'draft',
'is_featured' => false,
'sort_order' => 0,
'download_count' => 0,
'latest_version' => '',
]);
$uploadedFile = $this->makeZipUpload($package->slug, [
'Plugin.php' => '<?php echo "ok";',
]);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('manifest.json not found at zip root; version is required for zip publish');
app(VersionPublishService::class)->publishFromZip($package, $uploadedFile, []);
}
protected function tearDown(): void
{
foreach ($this->temporaryZipPaths as $path) {
if (is_file($path)) {
@unlink($path);
}
}
$storedZip = storage_path('app/packages/plugin/DemoPlugin/1.2.3.zip');
if (is_file($storedZip)) {
@unlink($storedZip);
}
$storedDir = storage_path('app/packages/plugin/DemoPlugin');
if (is_dir($storedDir)) {
@rmdir($storedDir);
}
parent::tearDown();
}
private function makeZipUpload(string $slug, array $files): UploadedFile
{
$directory = storage_path('framework/testing');
File::ensureDirectoryExists($directory);
$path = tempnam($directory, 'zip');
$this->temporaryZipPaths[] = $path;
$zip = new ZipArchive();
$opened = $zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE);
if ($opened !== true) {
throw new RuntimeException('failed to create test zip');
}
foreach ($files as $name => $contents) {
$zip->addFromString($slug . '/' . $name, $contents);
}
$zip->close();
return new UploadedFile($path, $slug . '.zip', 'application/zip', null, true);
}
}