first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
<?php
namespace Common\Files\S3;
use Carbon\Carbon;
use Error;
use Illuminate\Console\Command;
use Illuminate\Filesystem\AwsS3V3Adapter;
use Illuminate\Support\Facades\Storage;
class AbortOldS3Uploads extends Command
{
use InteractsWithS3Api;
protected $signature = 's3:abort_expired';
protected $description = 'Abort and delete expired S3 file uploads';
public function handle(): int
{
try {
$client = $this->getClient();
} catch (Error $e) {
// if s3 is not configured or enabled, bail
$this->error(
'S3 is not configured or not selected as storage method in settings page.',
);
return 0;
}
$data = $client->listMultipartUploads([
'Bucket' => $this->getBucket(),
]);
$uploads = $data['Uploads'] ?: [];
foreach ($uploads as $upload) {
$createdAt = Carbon::parse($upload['Initiated']);
if ($createdAt->lessThanOrEqualTo(Carbon::now()->subDay())) {
$client->abortMultipartUpload([
'Bucket' => $this->getBucket(),
'Key' => $upload['Key'],
'UploadId' => $upload['UploadId'],
]);
}
}
$this->info('Expired uploads deleted from S3');
return Command::SUCCESS;
}
protected function getDiskName(): string
{
if (Storage::disk('uploads') instanceof AwsS3V3Adapter) {
return 'uploads';
}
return 'public';
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Common\Files\S3;
use Aws\S3\S3Client;
use Common\Settings\Settings;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
trait InteractsWithS3Api
{
protected function getDiskName(): string
{
return request()->input('disk') ?: 'uploads';
}
protected function getDisk(): Filesystem
{
return Storage::disk($this->getDiskName());
}
protected function getClient(): ?S3Client
{
return $this->getDisk()->getClient();
}
protected function getBucket(): string
{
$credentialsKey = config(
"common.site.{$this->getDiskName()}_disk_driver",
);
return config("services.{$credentialsKey}.bucket");
}
protected function getAcl(): string
{
return $this->getDiskName() === 'public' ||
config('common.site.remote_file_visibility') === 'public'
? 'public-read'
: 'private';
}
protected function buildFileKey(): string
{
$uuid = Str::uuid();
$filename = request('filename');
$extension = request('extension');
$keepOriginalName = app(Settings::class)->get(
'uploads.keep_original_name',
);
if ($this->getDiskName() === 'public') {
$fileKey = $keepOriginalName ? $filename : "$uuid.$extension";
$diskPrefix = request('diskPrefix');
if ($diskPrefix) {
$fileKey = "$diskPrefix/$fileKey";
}
} else {
$diskPrefix = $uuid;
$filename = $keepOriginalName ? $filename : $uuid;
$fileKey = "$diskPrefix/$filename";
}
$pathPrefix = $this->getDisk()->path('');
if ($pathPrefix) {
$fileKey = "{$pathPrefix}{$fileKey}";
}
return $fileKey;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Common\Files\S3;
use Common\Core\BaseController;
use Illuminate\Filesystem\AwsS3V3Adapter;
use Illuminate\Support\Facades\Storage;
class S3CorsController extends BaseController
{
use InteractsWithS3Api;
public function __construct()
{
$this->middleware('isAdmin');
}
public function uploadCors()
{
$cors = [
[
'AllowedOrigins' => [config('app.url')],
'AllowedMethods' => ['GET', 'HEAD', 'POST', 'PUT'],
'MaxAgeSeconds' => 3000,
'AllowedHeaders' => ['*'],
'ExposeHeaders' => ['ETag'],
],
];
$this->getClient()->putBucketCors([
'Bucket' => $this->getBucket(),
'CORSConfiguration' => [
'CORSRules' => $cors,
],
]);
return $this->success();
}
protected function getDiskName(): string
{
if (Storage::disk('uploads') instanceof AwsS3V3Adapter) {
return 'uploads';
}
return 'public';
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Common\Files\S3;
use Common\Core\BaseController;
use Common\Files\Actions\CreateFileEntry;
use Common\Files\Events\FileUploaded;
use Common\Files\FileEntry;
use Common\Files\FileEntryPayload;
class S3FileEntryController extends BaseController
{
public function store()
{
$validatedData = $this->validate(request(), [
'clientExtension' => 'required|string',
'clientMime' => 'nullable|string|max:255',
'clientName' => 'required|string',
'disk' => 'string',
'diskPrefix' => 'string',
'filename' => 'required|string',
'parentId' => 'nullable|exists:file_entries,id',
'relativePath' => 'nullable|string',
'workspaceId' => 'nullable|int',
'size' => 'required|int',
]);
$payload = new FileEntryPayload($validatedData);
$this->authorize('store', [FileEntry::class, $payload->parentId]);
$fileEntry = app(CreateFileEntry::class)->execute($payload);
event(new FileUploaded($fileEntry));
return $this->success(['fileEntry' => $fileEntry]);
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace Common\Files\S3;
use Carbon\Carbon;
use Common\Core\BaseController;
use Common\Files\Actions\ValidateFileUpload;
class S3MultipartUploadController extends BaseController
{
use InteractsWithS3Api;
public function __construct()
{
$this->middleware('auth');
}
public function create()
{
$errors = app(ValidateFileUpload::class)->execute(request()->all());
if ($errors) {
abort(422, $errors->first());
}
$result = $this->getClient()->createMultipartUpload([
'Key' => $this->buildFileKey(),
'Bucket' => $this->getBucket(),
'ContentType' => request()->input('mime'),
'ACL' => $this->getAcl(),
]);
return $this->success([
'key' => $result['Key'],
'uploadId' => $result['UploadId'],
'acl' => $this->getAcl(),
]);
}
public function getUploadedParts()
{
$data = $this->getClient()->listParts([
'Bucket' => $this->getBucket(),
'Key' => request('key'),
'UploadId' => request('uploadId'),
'PartNumberMarker' => 0,
]);
return $this->success([
'parts' => $data['Parts'],
]);
}
public function batchSignPartUrls()
{
$partNumbers = request()->input('partNumbers');
$urls = [];
foreach ($partNumbers as $partNumber) {
$url = $this->getPartUrl(
$partNumber,
request('uploadId'),
request('key'),
);
$urls[] = ['url' => $url, 'partNumber' => $partNumber];
}
return $this->success([
'urls' => $urls,
]);
}
public function complete()
{
$data = $this->getClient()->completeMultipartUpload([
'Bucket' => $this->getBucket(),
'Key' => request()->input('key'),
'UploadId' => request()->input('uploadId'),
'MultipartUpload' => [
'Parts' => request()->input('parts'),
],
]);
return $this->success([
'location' => $data['Location'],
]);
}
public function abort()
{
$this->getClient()->abortMultipartUpload([
'Bucket' => $this->getBucket(),
'Key' => request()->input('key'),
'UploadId' => request()->input('uploadId'),
]);
return $this->success();
}
protected function getPartUrl(
string $partNumber,
string $uploadId,
string $key
): string {
$command = $this->getClient()->getCommand('UploadPart', [
'Bucket' => $this->getBucket(),
'Key' => $key,
'UploadId' => $uploadId,
'PartNumber' => $partNumber,
]);
$s3Request = $this->getClient()->createPresignedRequest(
$command,
Carbon::now()->addMinutes(30),
);
return (string) $s3Request->getUri();
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Common\Files\S3;
use Carbon\Carbon;
use Common\Core\BaseController;
use Common\Files\Actions\ValidateFileUpload;
class S3SimpleUploadController extends BaseController
{
use InteractsWithS3Api;
public function __construct()
{
$this->middleware('auth');
}
public function presignPost()
{
$fileKey = $this->buildFileKey();
$errors = app(ValidateFileUpload::class)->execute(request()->all());
if ($errors) {
abort(422, $errors->first());
}
$command = $this->getClient()->getCommand('PutObject', [
'Bucket' => $this->getBucket(),
'ContentType' => request()->input('mime'),
'Key' => $fileKey,
'ACL' => $this->getAcl(),
]);
$uri = $this->getClient()
->createPresignedRequest($command, Carbon::now()->addHour())
->getUri();
return $this->success([
'url' => $uri,
'key' => $fileKey,
'acl' => $this->getAcl(),
]);
}
}