130
common/Files/Actions/CreateFileEntry.php
Executable file
130
common/Files/Actions/CreateFileEntry.php
Executable file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Files\Actions;
|
||||
|
||||
use App\Models\User;
|
||||
use Common\Files\Events\FileEntryCreated;
|
||||
use Common\Files\FileEntry;
|
||||
use Common\Files\FileEntryPayload;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CreateFileEntry
|
||||
{
|
||||
public function execute(FileEntryPayload $payload): FileEntry
|
||||
{
|
||||
$data = [
|
||||
'name' => $payload->clientName,
|
||||
'file_name' => $payload->filename,
|
||||
'mime' => $payload->clientMime,
|
||||
'file_size' => $payload->size,
|
||||
'parent_id' => $payload->parentId,
|
||||
'disk_prefix' => $payload->diskPrefix,
|
||||
'type' => $payload->type,
|
||||
'extension' => $payload->clientExtension,
|
||||
'public' => $payload->public,
|
||||
'workspace_id' => $payload->workspaceId,
|
||||
'owner_id' => $payload->ownerId,
|
||||
];
|
||||
|
||||
$entries = new Collection();
|
||||
|
||||
// uploading a folder
|
||||
if ($payload->relativePath && !$payload->public) {
|
||||
$path = $this->createPath($payload);
|
||||
$parent = $path['allParents']->last();
|
||||
if ($path['allParents']->isNotEmpty()) {
|
||||
$entries = $entries->merge($path['allParents']);
|
||||
$data['parent_id'] = $parent->id;
|
||||
}
|
||||
}
|
||||
|
||||
$fileEntry = FileEntry::create($data);
|
||||
|
||||
if (!$payload->public) {
|
||||
$fileEntry->generatePath();
|
||||
}
|
||||
|
||||
$entries = $entries->push($fileEntry);
|
||||
|
||||
$entryIds = $entries
|
||||
->mapWithKeys(function ($entry) {
|
||||
return [$entry->id => ['owner' => 1]];
|
||||
})
|
||||
->toArray();
|
||||
|
||||
User::find($payload->ownerId)
|
||||
->entries()
|
||||
->syncWithoutDetaching($entryIds);
|
||||
|
||||
if (isset($path['newlyCreated'])) {
|
||||
$path['newlyCreated']->each(function (FileEntry $entry) use (
|
||||
$payload,
|
||||
) {
|
||||
// make sure new folder gets attached to all
|
||||
// users who have access to the parent folder
|
||||
event(new FileEntryCreated($entry));
|
||||
});
|
||||
}
|
||||
|
||||
if (isset($parent) && $parent) {
|
||||
$fileEntry->setRelation('parent', $parent);
|
||||
} else {
|
||||
$fileEntry->load('parent');
|
||||
}
|
||||
|
||||
$entries->load('users');
|
||||
|
||||
event(new FileEntryCreated($fileEntry));
|
||||
|
||||
return $fileEntry;
|
||||
}
|
||||
|
||||
private function createPath(FileEntryPayload $payload): array
|
||||
{
|
||||
$newlyCreated = collect();
|
||||
$dirname = dirname($payload->relativePath);
|
||||
// remove file name from path and split into folder names
|
||||
$path = collect(
|
||||
explode('/', $dirname === '.' ? $payload->relativePath : $dirname),
|
||||
)->filter();
|
||||
if ($path->isEmpty()) {
|
||||
return $path->toArray();
|
||||
}
|
||||
|
||||
$allParents = $path->reduce(function ($parents, $name) use (
|
||||
$newlyCreated,
|
||||
$payload,
|
||||
) {
|
||||
if (!$parents) {
|
||||
$parents = collect();
|
||||
}
|
||||
$parent = $parents->last();
|
||||
|
||||
$values = [
|
||||
'type' => 'folder',
|
||||
'name' => $name,
|
||||
// file name is limited to 36 chars in database, make sure we match that if we get very long file names
|
||||
'file_name' => Str::limit($name, 36, ''),
|
||||
'parent_id' => $parent ? $parent->id : $payload->parentId,
|
||||
'workspace_id' => $payload->workspaceId ?? 0,
|
||||
];
|
||||
|
||||
// check if user already has a folder with that name and parent
|
||||
$folder = FileEntry::where($values)
|
||||
->whereUser($payload->ownerId)
|
||||
->first();
|
||||
|
||||
if (!$folder) {
|
||||
$values['owner_id'] = $payload->ownerId;
|
||||
$folder = FileEntry::create($values);
|
||||
$folder->generatePath();
|
||||
$newlyCreated->push($folder);
|
||||
}
|
||||
|
||||
return $parents->push($folder);
|
||||
});
|
||||
|
||||
return ['allParents' => $allParents, 'newlyCreated' => $newlyCreated];
|
||||
}
|
||||
}
|
||||
40
common/Files/Actions/Deletion/DeleteEntries.php
Executable file
40
common/Files/Actions/Deletion/DeleteEntries.php
Executable file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Files\Actions\Deletion;
|
||||
|
||||
use Arr;
|
||||
use Common\Files\FileEntry;
|
||||
use Gate;
|
||||
|
||||
class DeleteEntries
|
||||
{
|
||||
public function execute(array $params, $authorize = true): void
|
||||
{
|
||||
$entryIds =
|
||||
$params['entryIds'] ?? $this->idsFromPaths($params['paths']);
|
||||
|
||||
if (count($entryIds)) {
|
||||
if ($authorize) {
|
||||
Gate::authorize('destroy', [FileEntry::class, $entryIds]);
|
||||
}
|
||||
|
||||
if (Arr::get($params, 'soft')) {
|
||||
app(SoftDeleteEntries::class)->execute($entryIds);
|
||||
} else {
|
||||
app(PermanentlyDeleteEntries::class)->execute($entryIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function idsFromPaths(array $paths): array
|
||||
{
|
||||
$filenames = array_map(function ($path) {
|
||||
return basename($path);
|
||||
}, $paths);
|
||||
|
||||
return app(FileEntry::class)
|
||||
->whereIn('file_name', $filenames)
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
61
common/Files/Actions/Deletion/PermanentlyDeleteEntries.php
Executable file
61
common/Files/Actions/Deletion/PermanentlyDeleteEntries.php
Executable file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Files\Actions\Deletion;
|
||||
|
||||
use Common\Files\Events\FileEntriesDeleted;
|
||||
use Common\Files\FileEntry;
|
||||
use DB;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class PermanentlyDeleteEntries extends SoftDeleteEntries
|
||||
{
|
||||
/**
|
||||
* Permanently delete file entries, related records and files from disk.
|
||||
*/
|
||||
protected function delete(Collection|array $entries): void
|
||||
{
|
||||
$entries = $this->loadChildEntries($entries, true);
|
||||
$this->deleteFiles($entries);
|
||||
$this->deleteEntries($entries);
|
||||
event(new FileEntriesDeleted($entries->pluck('id')->toArray(), true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete file entries from database.
|
||||
*/
|
||||
private function deleteEntries(Collection $entries): void
|
||||
{
|
||||
$entryIds = $entries->pluck('id');
|
||||
|
||||
// detach users
|
||||
DB::table('file_entry_models')
|
||||
->whereIn('file_entry_id', $entryIds)
|
||||
->delete();
|
||||
|
||||
// detach tags
|
||||
DB::table('taggables')
|
||||
->where('taggable_type', FileEntry::MODEL_TYPE)
|
||||
->whereIn('taggable_id', $entryIds)
|
||||
->delete();
|
||||
|
||||
$this->entry->whereIn('id', $entries->pluck('id'))->forceDelete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete files from disk.
|
||||
*/
|
||||
private function deleteFiles(Collection $entries): void
|
||||
{
|
||||
$entries
|
||||
->filter(function (FileEntry $entry) {
|
||||
return $entry->type !== 'folder';
|
||||
})
|
||||
->each(function (FileEntry $entry) {
|
||||
if ($entry->public) {
|
||||
$entry->getDisk()->delete($entry->getStoragePath());
|
||||
} else {
|
||||
$entry->getDisk()->deleteDirectory($entry->file_name);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
22
common/Files/Actions/Deletion/RestoreEntries.php
Executable file
22
common/Files/Actions/Deletion/RestoreEntries.php
Executable file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Files\Actions\Deletion;
|
||||
|
||||
use Common\Files\Events\FileEntriesRestored;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class RestoreEntries extends SoftDeleteEntries
|
||||
{
|
||||
public function execute(Collection|array $entryIds): void
|
||||
{
|
||||
$entries = $this->entry
|
||||
->onlyTrashed()
|
||||
->whereIn('id', $entryIds)
|
||||
->get();
|
||||
$entries = $this->loadChildEntries($entries, true);
|
||||
|
||||
$this->entry->whereIn('id', $entries->pluck('id'))->restore();
|
||||
|
||||
event(new FileEntriesRestored($entries->pluck('id')->toArray()));
|
||||
}
|
||||
}
|
||||
40
common/Files/Actions/Deletion/SoftDeleteEntries.php
Executable file
40
common/Files/Actions/Deletion/SoftDeleteEntries.php
Executable file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Files\Actions\Deletion;
|
||||
|
||||
use Common\Files\Events\FileEntriesDeleted;
|
||||
use Common\Files\FileEntry;
|
||||
use Common\Files\Traits\LoadsAllChildEntries;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class SoftDeleteEntries
|
||||
{
|
||||
use LoadsAllChildEntries;
|
||||
|
||||
public function __construct(protected FileEntry $entry)
|
||||
{
|
||||
}
|
||||
|
||||
public function execute(Collection|array $entryIds): void
|
||||
{
|
||||
collect($entryIds)
|
||||
->chunk(50)
|
||||
->each(function ($ids) {
|
||||
$entries = $this->entry
|
||||
->withTrashed()
|
||||
->whereIn('id', $ids)
|
||||
->get();
|
||||
$this->delete($entries);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Move specified entries to "trash".
|
||||
*/
|
||||
protected function delete(Collection|array $entries): void
|
||||
{
|
||||
$entries = $this->loadChildEntries($entries);
|
||||
$this->entry->whereIn('id', $entries->pluck('id'))->delete();
|
||||
event(new FileEntriesDeleted($entries->pluck('id')->toArray(), false));
|
||||
}
|
||||
}
|
||||
48
common/Files/Actions/GetServerMaxUploadSize.php
Executable file
48
common/Files/Actions/GetServerMaxUploadSize.php
Executable file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Files\Actions;
|
||||
|
||||
class GetServerMaxUploadSize
|
||||
{
|
||||
protected $configKeys = ['post_max_size', 'upload_max_filesize', 'memory_limit'];
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function execute()
|
||||
{
|
||||
$configValues = collect($this->configKeys)
|
||||
->map(function($key) {
|
||||
$value = ini_get($key);
|
||||
return ['original' => $value, 'bytes' => $this->getBytes($value)];
|
||||
})->filter(function($value) {
|
||||
return $value['bytes'] > 0;
|
||||
});
|
||||
|
||||
return $configValues->where('bytes', $configValues->min('bytes'))->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|string $value
|
||||
* @return int
|
||||
*/
|
||||
protected function getBytes($value)
|
||||
{
|
||||
if (is_numeric($value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
$metric = strtoupper(substr($value, -1));
|
||||
|
||||
switch ($metric) {
|
||||
case 'K':
|
||||
return (int) $value * 1024;
|
||||
case 'M':
|
||||
return (int) $value * 1048576;
|
||||
case 'G':
|
||||
return (int) $value * 1073741824;
|
||||
default:
|
||||
return (int) $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
69
common/Files/Actions/GetUserSpaceUsage.php
Executable file
69
common/Files/Actions/GetUserSpaceUsage.php
Executable file
@@ -0,0 +1,69 @@
|
||||
<?php namespace Common\Files\Actions;
|
||||
|
||||
use App\Models\User;
|
||||
use Common\Billing\Models\Product;
|
||||
use Common\Settings\Settings;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class GetUserSpaceUsage
|
||||
{
|
||||
protected User $user;
|
||||
|
||||
public function __construct(protected Settings $settings)
|
||||
{
|
||||
$this->user = Auth::user();
|
||||
}
|
||||
|
||||
public function execute(User $user = null): array
|
||||
{
|
||||
$this->user = $user ?? Auth::user();
|
||||
return [
|
||||
'used' => $this->getSpaceUsed(),
|
||||
'available' => $this->getAvailableSpace(),
|
||||
];
|
||||
}
|
||||
|
||||
private function getSpaceUsed(): int|float
|
||||
{
|
||||
return (int) $this->user
|
||||
->entries(['owner' => true])
|
||||
->where('type', '!=', 'folder')
|
||||
->withTrashed()
|
||||
->sum('file_size');
|
||||
}
|
||||
|
||||
public function getAvailableSpace(): int|float|null
|
||||
{
|
||||
$space = null;
|
||||
|
||||
if (!is_null($this->user->available_space)) {
|
||||
$space = $this->user->available_space;
|
||||
} elseif (app(Settings::class)->get('billing.enable')) {
|
||||
if ($this->user->subscribed()) {
|
||||
$space = $this->user->subscriptions->first()->product
|
||||
->available_space;
|
||||
} elseif ($freePlan = Product::where('free', true)->first()) {
|
||||
$space = $freePlan->available_space;
|
||||
}
|
||||
}
|
||||
|
||||
// space is not set at all on user or billing plans
|
||||
if (is_null($space)) {
|
||||
$defaultSpace = $this->settings->get('uploads.available_space');
|
||||
return is_numeric($defaultSpace) ? abs($defaultSpace) : null;
|
||||
} else {
|
||||
return abs($space);
|
||||
}
|
||||
}
|
||||
|
||||
public function hasEnoughSpaceToUpload(int $bytes): bool
|
||||
{
|
||||
$availableSpace = $this->getAvailableSpace();
|
||||
|
||||
// unlimited space
|
||||
if (is_null($availableSpace)) {
|
||||
return true;
|
||||
}
|
||||
return $this->getSpaceUsed() + $bytes <= $availableSpace;
|
||||
}
|
||||
}
|
||||
124
common/Files/Actions/StoreFile.php
Executable file
124
common/Files/Actions/StoreFile.php
Executable file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Files\Actions;
|
||||
|
||||
use Common\Files\FileEntryPayload;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Illuminate\Http\File;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\File as FileFacade;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use League\Flysystem\Local\LocalFilesystemAdapter;
|
||||
use Symfony\Component\Mime\MimeTypes;
|
||||
|
||||
class StoreFile
|
||||
{
|
||||
protected Filesystem $disk;
|
||||
protected array $diskOptions;
|
||||
protected FileEntryPayload $payload;
|
||||
|
||||
public function execute(
|
||||
FileEntryPayload $payload,
|
||||
array $fileOptions,
|
||||
): string|false {
|
||||
$this->disk = $payload->public
|
||||
? Storage::disk('public')
|
||||
: Storage::disk('uploads');
|
||||
|
||||
$this->diskOptions = [
|
||||
'mimetype' => $payload->clientMime,
|
||||
'visibility' => $payload->visibility,
|
||||
];
|
||||
|
||||
$this->payload = $payload;
|
||||
|
||||
if (
|
||||
// prevent uploading .htaccess files
|
||||
$payload->filename === '.htaccess' ||
|
||||
// dont store php files in public disk
|
||||
($payload->public && $this->isPhpFile($payload, $fileOptions)) ||
|
||||
// prevent path traversal in user specified folder
|
||||
($payload->diskPrefix && Str::contains($payload->diskPrefix, '..'))
|
||||
) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
if (isset($fileOptions['file'])) {
|
||||
return $this->storeUploadedFile($fileOptions['file']);
|
||||
} elseif (isset($fileOptions['contents'])) {
|
||||
return $this->storeStringContents($fileOptions['contents']);
|
||||
} elseif (isset($fileOptions['path'])) {
|
||||
// if source and destination is local (and not temp dir) move file
|
||||
// instead of copying or using streams, this will be a lot faster
|
||||
if (
|
||||
Arr::get($fileOptions, 'moveFile') === true &&
|
||||
$this->disk->getAdapter() instanceof LocalFilesystemAdapter
|
||||
) {
|
||||
return $this->storeLocalFile($fileOptions['path']);
|
||||
} else {
|
||||
return $this->storeUploadedFile(new File($fileOptions['path']));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function storeUploadedFile(File|UploadedFile $file): string|false
|
||||
{
|
||||
return $this->disk->putFileAs(
|
||||
$this->payload->diskPrefix,
|
||||
$file,
|
||||
$this->payload->filename,
|
||||
$this->diskOptions,
|
||||
);
|
||||
}
|
||||
|
||||
protected function storeStringContents(string $contents): string|false
|
||||
{
|
||||
return $this->disk->put(
|
||||
"{$this->payload->diskPrefix}/{$this->payload->filename}",
|
||||
$contents,
|
||||
$this->diskOptions,
|
||||
);
|
||||
}
|
||||
|
||||
protected function storeLocalFile(string $sourcePath): string|false
|
||||
{
|
||||
$dirPath = $this->disk->path($this->payload->diskPrefix);
|
||||
|
||||
FileFacade::ensureDirectoryExists($dirPath);
|
||||
$stored = @rename($sourcePath, "$dirPath/{$this->payload->filename}");
|
||||
|
||||
if ($stored) {
|
||||
return "{$this->payload->diskPrefix}/{$this->payload->filename}";
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function isPhpFile(
|
||||
FileEntryPayload $payload,
|
||||
array $fileOptions,
|
||||
): bool {
|
||||
if (
|
||||
Str::of($payload->clientExtension)
|
||||
->lower()
|
||||
->startsWith(['php', 'phtml'])
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$mimeType = null;
|
||||
if (isset($fileOptions['file'])) {
|
||||
$mimeType = $fileOptions['file']->getMimeType();
|
||||
} elseif (isset($fileOptions['path'])) {
|
||||
$mimeType = MimeTypes::getDefault()->guessMimeType(
|
||||
$fileOptions['path'],
|
||||
);
|
||||
}
|
||||
|
||||
return $mimeType === 'application/x-php';
|
||||
}
|
||||
}
|
||||
138
common/Files/Actions/ValidateFileUpload.php
Executable file
138
common/Files/Actions/ValidateFileUpload.php
Executable file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Files\Actions;
|
||||
|
||||
use Common\Settings\Settings;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ValidateFileUpload
|
||||
{
|
||||
protected array $fileData;
|
||||
|
||||
public function execute(array $fileData): Collection|null
|
||||
{
|
||||
$this->fileData = $fileData;
|
||||
|
||||
$errors = collect([
|
||||
'size' => $this->validateMaximumFileSize(),
|
||||
'spaceUsage' => $this->validateAllowedStorageSpace(),
|
||||
'allowedExtensions' => $this->validateAllowedExtensions(),
|
||||
'blockedExtensions' => $this->validateBlockedExtensions(),
|
||||
])->filter(fn($msg) => !is_null($msg));
|
||||
|
||||
if (!$errors->isEmpty()) {
|
||||
return $errors;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function validateAllowedExtensions(): string|null
|
||||
{
|
||||
$allowedExtensions = settings('uploads.allowed_extensions');
|
||||
|
||||
if (
|
||||
!empty($allowedExtensions) &&
|
||||
!$this->extensionMatches($allowedExtensions)
|
||||
) {
|
||||
return __('Files of this type are not allowed');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function validateBlockedExtensions(): string|null
|
||||
{
|
||||
$blockedExtensions = settings('uploads.blocked_extensions');
|
||||
|
||||
if (
|
||||
!empty($blockedExtensions) &&
|
||||
$this->extensionMatches($blockedExtensions)
|
||||
) {
|
||||
return __('Files of this type are not allowed');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function extensionMatches(array $extensions): bool
|
||||
{
|
||||
if (empty($extensions) || !isset($this->fileData['extension'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$extensions = array_map(
|
||||
fn($ext) => str_replace('.', '', $ext),
|
||||
$extensions,
|
||||
);
|
||||
|
||||
return in_array(
|
||||
str_replace('.', '', $this->fileData['extension']),
|
||||
$extensions,
|
||||
);
|
||||
}
|
||||
|
||||
protected function validateMaximumFileSize(): ?string
|
||||
{
|
||||
$maxSize = app(Settings::class)->get('uploads.max_size');
|
||||
if (is_null($maxSize) || !isset($this->fileData['size'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((int) $this->fileData['size'] > (int) $maxSize) {
|
||||
return __('The file size may not be greater than :size', [
|
||||
'size' => self::formatBytes((int) $maxSize),
|
||||
]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function validateAllowedStorageSpace(): string|null
|
||||
{
|
||||
if (!isset($this->fileData['size']) || !Auth::check()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$enoughSpace = app(GetUserSpaceUsage::class)->hasEnoughSpaceToUpload(
|
||||
$this->fileData['size'],
|
||||
);
|
||||
|
||||
if (!$enoughSpace) {
|
||||
return self::notEnoughSpaceMessage();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function formatBytes(?int $bytes, $unit = 'MB'): string
|
||||
{
|
||||
if (is_null($bytes)) {
|
||||
return '0 bytes';
|
||||
}
|
||||
|
||||
if ((!$unit && $bytes >= 1 << 30) || $unit == 'GB') {
|
||||
return number_format($bytes / (1 << 30), 1) . 'GB';
|
||||
}
|
||||
if ((!$unit && $bytes >= 1 << 20) || $unit == 'MB') {
|
||||
return number_format($bytes / (1 << 20), 1) . 'MB';
|
||||
}
|
||||
if ((!$unit && $bytes >= 1 << 10) || $unit == 'KB') {
|
||||
return number_format($bytes / (1 << 10), 1) . 'KB';
|
||||
}
|
||||
return number_format($bytes) . ' bytes';
|
||||
}
|
||||
|
||||
public static function notEnoughSpaceMessage(): string
|
||||
{
|
||||
return __(
|
||||
'You have exhausted your allowed space of :space. Delete some files or upgrade your plan.',
|
||||
[
|
||||
'space' => self::formatBytes(
|
||||
app(GetUserSpaceUsage::class)->getAvailableSpace(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user