61
common/Files/S3/AbortOldS3Uploads.php
Executable file
61
common/Files/S3/AbortOldS3Uploads.php
Executable 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';
|
||||
}
|
||||
}
|
||||
73
common/Files/S3/InteractsWithS3Api.php
Executable file
73
common/Files/S3/InteractsWithS3Api.php
Executable 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;
|
||||
}
|
||||
}
|
||||
47
common/Files/S3/S3CorsController.php
Executable file
47
common/Files/S3/S3CorsController.php
Executable 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';
|
||||
}
|
||||
}
|
||||
38
common/Files/S3/S3FileEntryController.php
Executable file
38
common/Files/S3/S3FileEntryController.php
Executable 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]);
|
||||
}
|
||||
}
|
||||
118
common/Files/S3/S3MultipartUploadController.php
Executable file
118
common/Files/S3/S3MultipartUploadController.php
Executable 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();
|
||||
}
|
||||
}
|
||||
44
common/Files/S3/S3SimpleUploadController.php
Executable file
44
common/Files/S3/S3SimpleUploadController.php
Executable 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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user