feat: add s3 storages

This commit is contained in:
Andras Bacsai 2023-08-07 15:31:42 +02:00
parent 2a7e7e978b
commit f6e888ecf9
24 changed files with 916 additions and 35 deletions

View File

@ -8,6 +8,7 @@
use App\Models\Server;
use App\Models\TeamInvitation;
use App\Models\User;
use App\Models\S3Storage;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
@ -39,7 +40,7 @@ public function dashboard()
{
$projects = Project::ownedByCurrentTeam()->get();
$servers = Server::ownedByCurrentTeam()->get();
$s3s = S3Storage::ownedByCurrentTeam()->get();
$resources = 0;
foreach ($projects as $project) {
$resources += $project->applications->count();
@ -49,6 +50,7 @@ public function dashboard()
'servers' => $servers->count(),
'projects' => $projects->count(),
'resources' => $resources,
's3s' => $s3s,
]);
}
public function settings()
@ -83,6 +85,18 @@ public function team()
'invitations' => $invitations,
]);
}
public function storages() {
$s3 = S3Storage::ownedByCurrentTeam()->get();
return view('team.storages.all', [
's3' => $s3,
]);
}
public function storages_show() {
$storage = S3Storage::ownedByCurrentTeam()->whereUuid(request()->storage_uuid)->firstOrFail();
return view('team.storages.show', [
'storage' => $storage,
]);
}
public function members()
{
$invitations = [];
@ -136,4 +150,4 @@ public function revokeInvitation()
throw $th;
}
}
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Http\Livewire;
use App\Models\S3Storage;
use Illuminate\Support\Facades\Storage;
use Livewire\WithFileUploads;
use Livewire\Component;
class S3Test extends Component
{
use WithFileUploads;
public $s3;
public $file;
public function mount() {
$this->s3 = S3Storage::first();
ray($this->s3);
}
public function save() {
try {
$this->validate([
'file' => 'required|max:150', // 1MB Max
]);
set_s3_target($this->s3);
$this->file->storeAs('files', $this->file->getClientOriginalName(),'custom-s3');
$this->emit('success', 'File uploaded successfully.');
} catch (\Throwable $th) {
return general_error_handler($th, $this, false);
}
}
public function get_files()
{
set_s3_target($this->s3);
dd(Storage::disk('custom-s3')->files('files'));
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Http\Livewire\Team\Storage;
use Livewire\Component;
use App\Models\S3Storage;
class Create extends Component
{
public string $name;
public string $description;
public string $region = 'us-east-1';
public string $key;
public string $secret;
public string $bucket;
public string $endpoint;
public S3Storage $storage;
protected $rules = [
'name' => 'nullable|min:3|max:255',
'description' => 'nullable|min:3|max:255',
'region' => 'required|max:255',
'key' => 'required|max:255',
'secret' => 'required|max:255',
'bucket' => 'required|max:255',
'endpoint' => 'nullable|url|max:255',
];
protected $validationAttributes = [
'name' => 'Name',
'description' => 'Description',
'region' => 'Region',
'key' => 'Key',
'secret' => "Secret",
'bucket' => 'Bucket',
'endpoint' => 'Endpoint',
];
public function mount() {
if (isDev()) {
$this->name = 'Local MinIO';
$this->description = 'Local MinIO';
$this->key = 'minioadmin';
$this->secret = 'minioadmin';
$this->bucket = 'local';
$this->endpoint = 'http://coolify-minio:9000';
}
}
private function test_s3_connection() {
try {
$this->storage->testConnection();
return $this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
} catch(\Throwable $th) {
return general_error_handler($th, $this);
}
}
public function submit() {
try {
$this->validate();
$this->storage = new S3Storage();
$this->storage->name = $this->name;
$this->storage->description = $this->description;
$this->storage->region = $this->region;
$this->storage->key = $this->key;
$this->storage->secret = $this->secret;
$this->storage->bucket = $this->bucket;
if (empty($this->endpoint)) {
$this->storage->endpoint = "https://s3.{$this->region}.amazonaws.com";
} else {
$this->storage->endpoint = $this->endpoint;
}
$this->storage->team_id = auth()->user()->currentTeam()->id;
$this->storage->testConnection();
$this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
$this->storage->save();
return redirect()->route('team.storages.show', $this->storage->uuid);
} catch(\Throwable $th) {
return general_error_handler($th, $this);
}
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Http\Livewire\Team\Storage;
use Livewire\Component;
use App\Models\S3Storage;
class Form extends Component
{
public S3Storage $storage;
protected $rules = [
'storage.name' => 'nullable|min:3|max:255',
'storage.description' => 'nullable|min:3|max:255',
'storage.region' => 'required|max:255',
'storage.key' => 'required|max:255',
'storage.secret' => 'required|max:255',
'storage.bucket' => 'required|max:255',
'storage.endpoint' => 'required|url|max:255',
];
protected $validationAttributes = [
'storage.name' => 'Name',
'storage.description' => 'Description',
'storage.region' => 'Region',
'storage.key' => 'Key',
'storage.secret' => "Secret",
'storage.bucket' => 'Bucket',
'storage.endpoint' => 'Endpoint',
];
public function test_s3_connection() {
try {
$this->storage->testConnection();
return $this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
} catch(\Throwable $th) {
return general_error_handler($th, $this);
}
}
public function delete() {
try {
$this->storage->delete();
return redirect()->route('team.storages.all');
} catch(\Throwable $th) {
return general_error_handler($th, $this);
}
}
public function submit()
{
$this->validate();
try {
$this->storage->testConnection();
$this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
$this->storage->save();
$this->emit('success', 'Storage settings saved.');
} catch (\Throwable $th) {
return general_error_handler($th, $this);
}
}
}

25
app/Models/S3Storage.php Normal file
View File

@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class S3Storage extends BaseModel
{
use HasFactory;
protected $guarded = [];
static public function ownedByCurrentTeam(array $select = ['*'])
{
$selectArray = collect($select)->concat(['id']);
return S3Storage::whereTeamId(session('currentTeam')->id)->select($selectArray->all())->orderBy('name');
}
public function awsUrl() {
return "{$this->endpoint}/{$this->bucket}";
}
public function testConnection() {
set_s3_target($this);
return \Storage::disk('custom-s3')->files();
}
}

22
bootstrap/helpers/s3.php Normal file
View File

@ -0,0 +1,22 @@
<?php
use App\Models\S3Storage;
use Illuminate\Support\Str;
function set_s3_target(S3Storage $s3) {
$is_digital_ocean = false;
if ($s3->endpoint) {
$is_digital_ocean = Str::contains($s3->endpoint,'digitaloceanspaces.com');
}
config()->set('filesystems.disks.custom-s3', [
'driver' => 's3',
'region' => $s3['region'],
'key' => $s3['key'],
'secret' => $s3['secret'],
'bucket' => $s3['bucket'],
'endpoint' => $s3['endpoint'],
'use_path_style_endpoint' => true,
'bucket_endpoint' => $is_digital_ocean,
'aws_url' => $s3->awsUrl(),
]);
}

View File

@ -18,6 +18,7 @@
"laravel/tinker": "^v2.8.1",
"laravel/ui": "^4.2",
"lcobucci/jwt": "^5.0.0",
"league/flysystem-aws-s3-v3": "^3.0",
"livewire/livewire": "^v2.12.3",
"masmerise/livewire-toaster": "^1.2",
"nubs/random-name-generator": "^2.2",

332
composer.lock generated
View File

@ -4,8 +4,157 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "fa99d49c392ba6560f15eb6f3c5e4baa",
"content-hash": "ca293cd95f3fcb41f9122aaf474adb69",
"packages": [
{
"name": "aws/aws-crt-php",
"version": "v1.2.2",
"source": {
"type": "git",
"url": "https://github.com/awslabs/aws-crt-php.git",
"reference": "2f1dc7b7eda080498be96a4a6d683a41583030e9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/2f1dc7b7eda080498be96a4a6d683a41583030e9",
"reference": "2f1dc7b7eda080498be96a4a6d683a41583030e9",
"shasum": ""
},
"require": {
"php": ">=5.5"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35||^5.6.3||^9.5",
"yoast/phpunit-polyfills": "^1.0"
},
"suggest": {
"ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality."
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "AWS SDK Common Runtime Team",
"email": "aws-sdk-common-runtime@amazon.com"
}
],
"description": "AWS Common Runtime for PHP",
"homepage": "https://github.com/awslabs/aws-crt-php",
"keywords": [
"amazon",
"aws",
"crt",
"sdk"
],
"support": {
"issues": "https://github.com/awslabs/aws-crt-php/issues",
"source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.2"
},
"time": "2023-07-20T16:49:55+00:00"
},
{
"name": "aws/aws-sdk-php",
"version": "3.269.0",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "6d759ef9f24f0c7f271baf8014f41fc0cfdfbf78"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6d759ef9f24f0c7f271baf8014f41fc0cfdfbf78",
"reference": "6d759ef9f24f0c7f271baf8014f41fc0cfdfbf78",
"shasum": ""
},
"require": {
"aws/aws-crt-php": "^1.0.4",
"ext-json": "*",
"ext-pcre": "*",
"ext-simplexml": "*",
"guzzlehttp/guzzle": "^6.5.8 || ^7.4.5",
"guzzlehttp/promises": "^1.4.0",
"guzzlehttp/psr7": "^1.9.1 || ^2.4.5",
"mtdowling/jmespath.php": "^2.6",
"php": ">=5.5"
},
"require-dev": {
"andrewsville/php-token-reflection": "^1.4",
"aws/aws-php-sns-message-validator": "~1.0",
"behat/behat": "~3.0",
"composer/composer": "^1.10.22",
"dms/phpunit-arraysubset-asserts": "^0.4.0",
"doctrine/cache": "~1.4",
"ext-dom": "*",
"ext-openssl": "*",
"ext-pcntl": "*",
"ext-sockets": "*",
"nette/neon": "^2.3",
"paragonie/random_compat": ">= 2",
"phpunit/phpunit": "^4.8.35 || ^5.6.3 || ^9.5",
"psr/cache": "^1.0",
"psr/http-message": "^1.0",
"psr/simple-cache": "^1.0",
"sebastian/comparator": "^1.2.3 || ^4.0",
"yoast/phpunit-polyfills": "^1.0"
},
"suggest": {
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
"doctrine/cache": "To use the DoctrineCacheAdapter",
"ext-curl": "To send requests using cURL",
"ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages",
"ext-sockets": "To use client-side monitoring"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"files": [
"src/functions.php"
],
"psr-4": {
"Aws\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Amazon Web Services",
"homepage": "http://aws.amazon.com"
}
],
"description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
"homepage": "http://aws.amazon.com/sdkforphp",
"keywords": [
"amazon",
"aws",
"cloud",
"dynamodb",
"ec2",
"glacier",
"s3",
"sdk"
],
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.269.0"
},
"time": "2023-04-26T18:21:04+00:00"
},
{
"name": "bacon/bacon-qr-code",
"version": "2.0.8",
@ -1206,33 +1355,29 @@
},
{
"name": "guzzlehttp/promises",
"version": "2.0.0",
"version": "1.5.3",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "3a494dc7dc1d7d12e511890177ae2d0e6c107da6"
"reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/3a494dc7dc1d7d12e511890177ae2d0e6c107da6",
"reference": "3a494dc7dc1d7d12e511890177ae2d0e6c107da6",
"url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e",
"reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
"php": ">=5.5"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.1",
"phpunit/phpunit": "^8.5.29 || ^9.5.23"
"symfony/phpunit-bridge": "^4.4 || ^5.1"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
}
@ -1269,7 +1414,7 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/2.0.0"
"source": "https://github.com/guzzle/promises/tree/1.5.3"
},
"funding": [
{
@ -1285,20 +1430,20 @@
"type": "tidelift"
}
],
"time": "2023-05-21T13:50:22+00:00"
"time": "2023-05-21T12:31:43+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "2.5.0",
"version": "2.6.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "b635f279edd83fc275f822a1188157ffea568ff6"
"reference": "8bd7c33a0734ae1c5d074360512beb716bef3f77"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/b635f279edd83fc275f822a1188157ffea568ff6",
"reference": "b635f279edd83fc275f822a1188157ffea568ff6",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/8bd7c33a0734ae1c5d074360512beb716bef3f77",
"reference": "8bd7c33a0734ae1c5d074360512beb716bef3f77",
"shasum": ""
},
"require": {
@ -1385,7 +1530,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.5.0"
"source": "https://github.com/guzzle/psr7/tree/2.6.0"
},
"funding": [
{
@ -1401,7 +1546,7 @@
"type": "tidelift"
}
],
"time": "2023-04-17T16:11:26+00:00"
"time": "2023-08-03T15:06:02+00:00"
},
{
"name": "guzzlehttp/uri-template",
@ -2553,6 +2698,72 @@
],
"time": "2023-05-04T09:04:26+00:00"
},
{
"name": "league/flysystem-aws-s3-v3",
"version": "3.15.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
"reference": "d8de61ee10b6a607e7996cff388c5a3a663e8c8a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/d8de61ee10b6a607e7996cff388c5a3a663e8c8a",
"reference": "d8de61ee10b6a607e7996cff388c5a3a663e8c8a",
"shasum": ""
},
"require": {
"aws/aws-sdk-php": "^3.220.0",
"league/flysystem": "^3.10.0",
"league/mime-type-detection": "^1.0.0",
"php": "^8.0.2"
},
"conflict": {
"guzzlehttp/guzzle": "<7.0",
"guzzlehttp/ringphp": "<1.1.1"
},
"type": "library",
"autoload": {
"psr-4": {
"League\\Flysystem\\AwsS3V3\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Frank de Jonge",
"email": "info@frankdejonge.nl"
}
],
"description": "AWS S3 filesystem adapter for Flysystem.",
"keywords": [
"Flysystem",
"aws",
"file",
"files",
"filesystem",
"s3",
"storage"
],
"support": {
"issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues",
"source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.15.0"
},
"funding": [
{
"url": "https://ecologi.com/frankdejonge",
"type": "custom"
},
{
"url": "https://github.com/frankdejonge",
"type": "github"
}
],
"time": "2023-05-02T20:02:14+00:00"
},
{
"name": "league/flysystem-local",
"version": "3.15.0",
@ -2615,26 +2826,26 @@
},
{
"name": "league/mime-type-detection",
"version": "1.11.0",
"version": "1.13.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/mime-type-detection.git",
"reference": "ff6248ea87a9f116e78edd6002e39e5128a0d4dd"
"reference": "a6dfb1194a2946fcdc1f38219445234f65b35c96"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ff6248ea87a9f116e78edd6002e39e5128a0d4dd",
"reference": "ff6248ea87a9f116e78edd6002e39e5128a0d4dd",
"url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/a6dfb1194a2946fcdc1f38219445234f65b35c96",
"reference": "a6dfb1194a2946fcdc1f38219445234f65b35c96",
"shasum": ""
},
"require": {
"ext-fileinfo": "*",
"php": "^7.2 || ^8.0"
"php": "^7.4 || ^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.2",
"phpstan/phpstan": "^0.12.68",
"phpunit/phpunit": "^8.5.8 || ^9.3"
"phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0"
},
"type": "library",
"autoload": {
@ -2655,7 +2866,7 @@
"description": "Mime-type detection for Flysystem",
"support": {
"issues": "https://github.com/thephpleague/mime-type-detection/issues",
"source": "https://github.com/thephpleague/mime-type-detection/tree/1.11.0"
"source": "https://github.com/thephpleague/mime-type-detection/tree/1.13.0"
},
"funding": [
{
@ -2667,7 +2878,7 @@
"type": "tidelift"
}
],
"time": "2022-04-17T13:12:02+00:00"
"time": "2023-08-05T12:09:49+00:00"
},
{
"name": "livewire/livewire",
@ -2911,6 +3122,67 @@
],
"time": "2023-06-21T08:46:11+00:00"
},
{
"name": "mtdowling/jmespath.php",
"version": "2.6.1",
"source": {
"type": "git",
"url": "https://github.com/jmespath/jmespath.php.git",
"reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/9b87907a81b87bc76d19a7fb2d61e61486ee9edb",
"reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb",
"shasum": ""
},
"require": {
"php": "^5.4 || ^7.0 || ^8.0",
"symfony/polyfill-mbstring": "^1.17"
},
"require-dev": {
"composer/xdebug-handler": "^1.4 || ^2.0",
"phpunit/phpunit": "^4.8.36 || ^7.5.15"
},
"bin": [
"bin/jp.php"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.6-dev"
}
},
"autoload": {
"files": [
"src/JmesPath.php"
],
"psr-4": {
"JmesPath\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Declaratively specify how to extract elements from a JSON document",
"keywords": [
"json",
"jsonpath"
],
"support": {
"issues": "https://github.com/jmespath/jmespath.php/issues",
"source": "https://github.com/jmespath/jmespath.php/tree/2.6.1"
},
"time": "2021-06-14T00:11:39+00:00"
},
{
"name": "nesbot/carbon",
"version": "2.68.1",

158
config/livewire.php Normal file
View File

@ -0,0 +1,158 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Class Namespace
|--------------------------------------------------------------------------
|
| This value sets the root namespace for Livewire component classes in
| your application. This value affects component auto-discovery and
| any Livewire file helper commands, like `artisan make:livewire`.
|
| After changing this item, run: `php artisan livewire:discover`.
|
*/
'class_namespace' => 'App\\Http\\Livewire',
/*
|--------------------------------------------------------------------------
| View Path
|--------------------------------------------------------------------------
|
| This value sets the path for Livewire component views. This affects
| file manipulation helper commands like `artisan make:livewire`.
|
*/
'view_path' => resource_path('views/livewire'),
/*
|--------------------------------------------------------------------------
| Layout
|--------------------------------------------------------------------------
| The default layout view that will be used when rendering a component via
| Route::get('/some-endpoint', SomeComponent::class);. In this case the
| the view returned by SomeComponent will be wrapped in "layouts.app"
|
*/
'layout' => 'layouts.app',
/*
|--------------------------------------------------------------------------
| Livewire Assets URL
|--------------------------------------------------------------------------
|
| This value sets the path to Livewire JavaScript assets, for cases where
| your app's domain root is not the correct path. By default, Livewire
| will load its JavaScript assets from the app's "relative root".
|
| Examples: "/assets", "myurl.com/app".
|
*/
'asset_url' => null,
/*
|--------------------------------------------------------------------------
| Livewire App URL
|--------------------------------------------------------------------------
|
| This value should be used if livewire assets are served from CDN.
| Livewire will communicate with an app through this url.
|
| Examples: "https://my-app.com", "myurl.com/app".
|
*/
'app_url' => null,
/*
|--------------------------------------------------------------------------
| Livewire Endpoint Middleware Group
|--------------------------------------------------------------------------
|
| This value sets the middleware group that will be applied to the main
| Livewire "message" endpoint (the endpoint that gets hit everytime
| a Livewire component updates). It is set to "web" by default.
|
*/
'middleware_group' => 'web',
/*
|--------------------------------------------------------------------------
| Livewire Temporary File Uploads Endpoint Configuration
|--------------------------------------------------------------------------
|
| Livewire handles file uploads by storing uploads in a temporary directory
| before the file is validated and stored permanently. All file uploads
| are directed to a global endpoint for temporary storage. The config
| items below are used for customizing the way the endpoint works.
|
*/
'temporary_file_upload' => [
'disk' => null, // Example: 'local', 's3' Default: 'default'
'rules' => null, // Example: ['file', 'mimes:png,jpg'] Default: ['required', 'file', 'max:12288'] (12MB)
'directory' => null, // Example: 'tmp' Default 'livewire-tmp'
'middleware' => null, // Example: 'throttle:5,1' Default: 'throttle:60,1'
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs.
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
'mov', 'avi', 'wmv', 'mp3', 'm4a',
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
],
'max_upload_time' => 5, // Max duration (in minutes) before an upload gets invalidated.
],
/*
|--------------------------------------------------------------------------
| Manifest File Path
|--------------------------------------------------------------------------
|
| This value sets the path to the Livewire manifest file.
| The default should work for most cases (which is
| "<app_root>/bootstrap/cache/livewire-components.php"), but for specific
| cases like when hosting on Laravel Vapor, it could be set to a different value.
|
| Example: for Laravel Vapor, it would be "/tmp/storage/bootstrap/cache/livewire-components.php".
|
*/
'manifest_path' => null,
/*
|--------------------------------------------------------------------------
| Back Button Cache
|--------------------------------------------------------------------------
|
| This value determines whether the back button cache will be used on pages
| that contain Livewire. By disabling back button cache, it ensures that
| the back button shows the correct state of components, instead of
| potentially stale, cached data.
|
| Setting it to "false" (default) will disable back button cache.
|
*/
'back_button_cache' => false,
/*
|--------------------------------------------------------------------------
| Render On Redirect
|--------------------------------------------------------------------------
|
| This value determines whether Livewire will render before it's redirected
| or not. Setting it to "false" (default) will mean the render method is
| skipped when redirecting. And "true" will mean the render method is
| run before redirecting. Browsers bfcache can store a potentially
| stale view if render is skipped on redirect.
|
*/
'render_on_redirect' => false,
];

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('s3_storages', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name')->nullable();
$table->longText('description')->nullable();
$table->string('region')->default('us-east-1');
$table->longText('key');
$table->longText('secret');
$table->longText('bucket');
$table->longText('endpoint')->nullable();
$table->foreignId('team_id');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('s3_storages');
}
};

View File

@ -31,6 +31,7 @@ public function run(): void
ServiceSeeder::class,
EnvironmentVariableSeeder::class,
LocalPersistentVolumeSeeder::class,
S3StorageSeeder::class,
]);
}
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\S3Storage;
class S3StorageSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
S3Storage::create([
'name' => 'Local MinIO',
'description' => 'Local MinIO S3 Storage',
'key' => 'minioadmin',
'secret' => 'minioadmin',
'bucket' => 'local',
'endpoint' => 'http://coolify-minio:9000',
'team_id' => 0,
]);
S3Storage::create([
'name' => 'DO Spaces',
'description' => 'DO S3 Storage',
'key' => 'DO003UBFTACPQGUXUANY',
'secret' => 'eXDSco/04+5RHti19X8O/QE1aWIhZHAyyuOEs4J1JWA',
'bucket' => 'files',
'endpoint' => 'https://test-coolify.ams3.digitaloceanspaces.com',
'team_id' => 0,
]);
}
}

View File

@ -69,7 +69,20 @@ services:
- "${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025"
networks:
- coolify
minio:
image: minio/minio:latest
container_name: coolify-minio
command: server /data --console-address ":9001"
ports:
- "${FORWARD_MINIO_PORT:-9000}:9000"
- "${FORWARD_MINIO_PORT_CONSOLE:-9001}:9001"
environment:
MINIO_ACCESS_KEY: "${MINIO_ACCESS_KEY:-minioadmin}"
MINIO_SECRET_KEY: "${MINIO_SECRET_KEY:-minioadmin}"
volumes:
- ./_data/_volumes/minio/:/data
networks:
- coolify
# buggregator:
# image: ghcr.io/buggregator/server:latest
# container_name: coolify-debug

View File

@ -27,6 +27,10 @@
<a class="{{ request()->routeIs('team.members') ? 'text-white' : '' }}" href="{{ route('team.members') }}">
<button>Members</button>
</a>
<a class="{{ request()->routeIs('team.storages.all') ? 'text-white' : '' }}"
href="{{ route('team.storages.all') }}">
<button>S3 Storages</button>
</a>
<a class="{{ request()->routeIs('team.notifications') ? 'text-white' : '' }}"
href="{{ route('team.notifications') }}">
<button>Notifications</button>

View File

@ -17,5 +17,12 @@
<div class="stat-value">{{ $resources }}</div>
<div class="stat-desc">Applications, databases, etc...</div>
</div>
<div class="stat">
<div class="stat-title">S3 Storages</div>
<div class="stat-value">{{ $s3s->count() }}</div>
</div>
</div>
@if (isDev())
<livewire:s3-test />
@endif
</x-layout>

View File

@ -0,0 +1,15 @@
<div>
<h2>S3 Test</h2>
<form wire:submit.prevent="save">
<input type="file" wire:model="file">
@error('file')
<span class="error">{{ $message }}</span>
@enderror
<div wire:loading wire:target="file">Uploading to server...</div>
@if ($file)
<x-forms.button type="submit">Upload file to s3:/files</x-forms.button>
@endif
</form>
<h4>Functions</h4>
<x-forms.button wire:click="get_files">Get s3:/files</x-forms.button>
</div>

View File

@ -0,0 +1,23 @@
<div>
<h1>Create a new S3 Storage</h1>
<div class="pt-2 pb-10 ">S3 Storage used to save backups / files</div>
<form class="flex flex-col gap-2" wire:submit.prevent='submit'>
<div class="flex gap-2">
<x-forms.input label="Name" id="name" />
<x-forms.input label="Description" id="description" />
</div>
<div class="flex gap-2">
<x-forms.input type="url" label="Endpoint" id="endpoint" />
<x-forms.input required label="Bucket" id="bucket" />
<x-forms.input required label="Region" id="region" />
</div>
<div class="flex gap-2">
<x-forms.input required type="password" label="Access Key" id="key" />
<x-forms.input required type="password" label="Secret Key" id="secret" />
</div>
<x-forms.button type="submit">
Save New S3 Storage
</x-forms.button>
</form>
</div>

View File

@ -0,0 +1,38 @@
<div>
<x-modal yesOrNo modalId="deleteS3Storage" modalTitle="Delete S3 Storage">
<x-slot:modalBody>
<p>This storage will be deleted. It is not reversible. Your data won't be touched!<br>Please think again..
</p>
</x-slot:modalBody>
</x-modal>
<form class="flex flex-col gap-2 pb-6" wire:submit.prevent='submit'>
<div class="flex items-start gap-2">
<div class="pb-4">
<h2>Storage Details</h2>
<div>{{ $storage->name }}</div>
</div>
<x-forms.button type="submit">
Save
</x-forms.button>
<x-forms.button wire:click="test_s3_connection">
Test Connection
</x-forms.button>
<x-forms.button isError isModal modalId="deleteS3Storage">
Delete
</x-forms.button>
</div>
<div class="flex gap-2">
<x-forms.input label="Name" id="storage.name" />
<x-forms.input label="Description" id="storage.description" />
</div>
<div class="flex gap-2">
<x-forms.input required label="Endpoint" id="storage.endpoint" />
<x-forms.input required label="Bucket" id="storage.bucket" />
<x-forms.input required label="Region" id="storage.region" />
</div>
<div class="flex gap-2">
<x-forms.input required type="password" label="Access Key" id="storage.key" />
<x-forms.input required type="password" label="Secret Key" id="storage.secret" />
</div>
</form>
</div>

View File

@ -1,6 +1,6 @@
<x-layout>
<x-team.navbar :team="session('currentTeam')" />
<h3>Members</h3>
<h2>Members</h2>
<div class="pt-4 overflow-hidden">
<table>
<thead>

View File

@ -1,5 +1,6 @@
<x-layout>
<x-team.navbar :team="session('currentTeam')" />
<h2 class="pb-4">Notifications</h2>
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'email' }" class="flex h-full">
<div class="flex flex-col gap-4 min-w-fit">
<a :class="activeTab === 'email' && 'text-white'"

View File

@ -0,0 +1,32 @@
<x-layout>
<x-team.navbar :team="session('currentTeam')" />
<div class="flex items-start gap-2">
<h2 class="pb-4">S3 Storages</h2>
<x-forms.button class="btn">
<a class="text-white hover:no-underline" href="/team/storages/new">+ Add</a>
</x-forms.button>
</div>
<div class="grid gap-2 lg:grid-cols-2">
@forelse ($s3 as $storage)
<div x-data x-on:click="goto('{{ $storage->uuid }}')" @class(['gap-2 border cursor-pointer box group border-transparent'])>
<div class="flex flex-col mx-6">
<div class=" group-hover:text-white">
{{ $storage->name }}
</div>
<div class="text-xs group-hover:text-white">
{{ $storage->description }}</div>
</div>
</div>
@empty
<div>
<div>No storage found.</div>
<x-use-magic-bar link="/team/storages/new" />
</div>
@endforelse
</div>
<script>
function goto(uuid) {
window.location.href = '/team/storages/' + uuid;
}
</script>
</x-layout>

View File

@ -0,0 +1,3 @@
<x-layout>
<livewire:team.storage.create />
</x-layout>

View File

@ -0,0 +1,4 @@
<x-layout>
<x-team.navbar :team="session('currentTeam')" />
<livewire:team.storage.form :storage="$storage" />
</x-layout>

View File

@ -92,6 +92,9 @@
Route::get('/team', [Controller::class, 'team'])->name('team.show');
Route::get('/team/new', fn () => view('team.create'))->name('team.create');
Route::get('/team/notifications', fn () => view('team.notifications'))->name('team.notifications');
Route::get('/team/storages', [Controller::class, 'storages'])->name('team.storages.all');
Route::get('/team/storages/new', fn () => view('team.storages.create'))->name('team.storages.new');
Route::get('/team/storages/{storage_uuid}', [Controller::class, 'storages_show'])->name('team.storages.show');
Route::get('/team/members', [Controller::class, 'members'])->name('team.members');
Route::get('/command-center', fn () => view('command-center', ['servers' => Server::isReachable()->get()]))->name('command-center');
Route::get('/invitations/{uuid}', [Controller::class, 'acceptInvitation'])->name('team.invitation.accept');