Split attachment service storage work out so it can be shared.
--- /dev/null
+<?php
+
+namespace BookStack\Exceptions;
+
+class ZipValidationException extends \Exception
+{
+ public function __construct(
+ public array $errors
+ ) {
+ parent::__construct();
+ }
+}
namespace BookStack\Exports\Controllers;
-use BookStack\Exports\Import;
-use BookStack\Exports\ZipExports\ZipExportReader;
-use BookStack\Exports\ZipExports\ZipExportValidator;
+use BookStack\Exceptions\ZipValidationException;
+use BookStack\Exports\ImportRepo;
use BookStack\Http\Controller;
+use BookStack\Uploads\AttachmentService;
use Illuminate\Http\Request;
class ImportController extends Controller
{
- public function __construct()
- {
+ public function __construct(
+ protected ImportRepo $imports,
+ ) {
$this->middleware('can:content-import');
}
public function upload(Request $request)
{
$this->validate($request, [
- 'file' => ['required', 'file']
+ 'file' => ['required', ...AttachmentService::getFileValidationRules()]
]);
$file = $request->file('file');
- $zipPath = $file->getRealPath();
-
- $errors = (new ZipExportValidator($zipPath))->validate();
- if ($errors) {
- session()->flash('validation_errors', $errors);
+ try {
+ $import = $this->imports->storeFromUpload($file);
+ } catch (ZipValidationException $exception) {
+ session()->flash('validation_errors', $exception->errors);
return redirect('/import');
}
- $zipEntityInfo = (new ZipExportReader($zipPath))->getEntityInfo();
- $import = new Import();
- $import->name = $zipEntityInfo['name'];
- $import->book_count = $zipEntityInfo['book_count'];
- $import->chapter_count = $zipEntityInfo['chapter_count'];
- $import->page_count = $zipEntityInfo['page_count'];
- $import->created_by = user()->id;
- $import->size = filesize($zipPath);
- // TODO - Set path
- // TODO - Save
-
- // TODO - Split out attachment service to separate out core filesystem/disk stuff
- // To reuse for import handling
-
- dd('passed');
- // TODO - Upload to storage
- // TODO - Store info/results for display:
- // TODO - Send user to next import stage
+ return redirect("imports/{$import->id}");
}
}
--- /dev/null
+<?php
+
+namespace BookStack\Exports;
+
+use BookStack\Exceptions\ZipValidationException;
+use BookStack\Exports\ZipExports\ZipExportReader;
+use BookStack\Exports\ZipExports\ZipExportValidator;
+use BookStack\Uploads\FileStorage;
+use Symfony\Component\HttpFoundation\File\UploadedFile;
+
+class ImportRepo
+{
+ public function __construct(
+ protected FileStorage $storage,
+ ) {
+ }
+
+ public function storeFromUpload(UploadedFile $file): Import
+ {
+ $zipPath = $file->getRealPath();
+
+ $errors = (new ZipExportValidator($zipPath))->validate();
+ if ($errors) {
+ throw new ZipValidationException($errors);
+ }
+
+ $zipEntityInfo = (new ZipExportReader($zipPath))->getEntityInfo();
+ $import = new Import();
+ $import->name = $zipEntityInfo['name'];
+ $import->book_count = $zipEntityInfo['book_count'];
+ $import->chapter_count = $zipEntityInfo['chapter_count'];
+ $import->page_count = $zipEntityInfo['page_count'];
+ $import->created_by = user()->id;
+ $import->size = filesize($zipPath);
+
+ $path = $this->storage->uploadFile(
+ $file,
+ 'uploads/files/imports/',
+ '',
+ 'zip'
+ );
+
+ $import->path = $path;
+ $import->save();
+
+ return $import;
+ }
+}
use BookStack\Exceptions\FileUploadException;
use Exception;
-use Illuminate\Contracts\Filesystem\Filesystem as Storage;
-use Illuminate\Filesystem\FilesystemManager;
-use Illuminate\Support\Facades\Log;
-use Illuminate\Support\Str;
-use League\Flysystem\WhitespacePathNormalizer;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class AttachmentService
{
public function __construct(
- protected FilesystemManager $fileSystem
+ protected FileStorage $storage,
) {
}
- /**
- * Get the storage that will be used for storing files.
- */
- protected function getStorageDisk(): Storage
- {
- return $this->fileSystem->disk($this->getStorageDiskName());
- }
-
- /**
- * Get the name of the storage disk to use.
- */
- protected function getStorageDiskName(): string
- {
- $storageType = config('filesystems.attachments');
-
- // Change to our secure-attachment disk if any of the local options
- // are used to prevent escaping that location.
- if ($storageType === 'local' || $storageType === 'local_secure' || $storageType === 'local_secure_restricted') {
- $storageType = 'local_secure_attachments';
- }
-
- return $storageType;
- }
-
- /**
- * Change the originally provided path to fit any disk-specific requirements.
- * This also ensures the path is kept to the expected root folders.
- */
- protected function adjustPathForStorageDisk(string $path): string
- {
- $path = (new WhitespacePathNormalizer())->normalizePath(str_replace('uploads/files/', '', $path));
-
- if ($this->getStorageDiskName() === 'local_secure_attachments') {
- return $path;
- }
-
- return 'uploads/files/' . $path;
- }
-
/**
* Stream an attachment from storage.
*
*/
public function streamAttachmentFromStorage(Attachment $attachment)
{
- return $this->getStorageDisk()->readStream($this->adjustPathForStorageDisk($attachment->path));
+ return $this->storage->getReadStream($attachment->path);
}
/**
*/
public function getAttachmentFileSize(Attachment $attachment): int
{
- return $this->getStorageDisk()->size($this->adjustPathForStorageDisk($attachment->path));
+ return $this->storage->getSize($attachment->path);
}
/**
* Delete a file from the filesystem it sits on.
* Cleans any empty leftover folders.
*/
- protected function deleteFileInStorage(Attachment $attachment)
+ protected function deleteFileInStorage(Attachment $attachment): void
{
- $storage = $this->getStorageDisk();
- $dirPath = $this->adjustPathForStorageDisk(dirname($attachment->path));
-
- $storage->delete($this->adjustPathForStorageDisk($attachment->path));
- if (count($storage->allFiles($dirPath)) === 0) {
- $storage->deleteDirectory($dirPath);
- }
+ $this->storage->delete($attachment->path);
}
/**
*/
protected function putFileInStorage(UploadedFile $uploadedFile): string
{
- $storage = $this->getStorageDisk();
$basePath = 'uploads/files/' . date('Y-m-M') . '/';
- $uploadFileName = Str::random(16) . '-' . $uploadedFile->getClientOriginalExtension();
- while ($storage->exists($this->adjustPathForStorageDisk($basePath . $uploadFileName))) {
- $uploadFileName = Str::random(3) . $uploadFileName;
- }
-
- $attachmentStream = fopen($uploadedFile->getRealPath(), 'r');
- $attachmentPath = $basePath . $uploadFileName;
-
- try {
- $storage->writeStream($this->adjustPathForStorageDisk($attachmentPath), $attachmentStream);
- } catch (Exception $e) {
- Log::error('Error when attempting file upload:' . $e->getMessage());
-
- throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentPath]));
- }
-
- return $attachmentPath;
+ return $this->storage->uploadFile(
+ $uploadedFile,
+ $basePath,
+ $uploadedFile->getClientOriginalExtension(),
+ ''
+ );
}
/**
* Get the file validation rules for attachments.
*/
- public function getFileValidationRules(): array
+ public static function getFileValidationRules(): array
{
return ['file', 'max:' . (config('app.upload_limit') * 1000)];
}
--- /dev/null
+<?php
+
+namespace BookStack\Uploads;
+
+use BookStack\Exceptions\FileUploadException;
+use Exception;
+use Illuminate\Contracts\Filesystem\Filesystem as Storage;
+use Illuminate\Filesystem\FilesystemManager;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Str;
+use League\Flysystem\WhitespacePathNormalizer;
+use Symfony\Component\HttpFoundation\File\UploadedFile;
+
+class FileStorage
+{
+ public function __construct(
+ protected FilesystemManager $fileSystem,
+ ) {
+ }
+
+ /**
+ * @return resource|null
+ */
+ public function getReadStream(string $path)
+ {
+ return $this->getStorageDisk()->readStream($this->adjustPathForStorageDisk($path));
+ }
+
+ public function getSize(string $path): int
+ {
+ return $this->getStorageDisk()->size($this->adjustPathForStorageDisk($path));
+ }
+
+ public function delete(string $path, bool $removeEmptyDir = false): void
+ {
+ $storage = $this->getStorageDisk();
+ $adjustedPath = $this->adjustPathForStorageDisk($path);
+ $dir = dirname($adjustedPath);
+
+ $storage->delete($adjustedPath);
+ if ($removeEmptyDir && count($storage->allFiles($dir)) === 0) {
+ $storage->deleteDirectory($dir);
+ }
+ }
+
+ /**
+ * @throws FileUploadException
+ */
+ public function uploadFile(UploadedFile $file, string $subDirectory, string $suffix, string $extension): string
+ {
+ $storage = $this->getStorageDisk();
+ $basePath = trim($subDirectory, '/') . '/';
+
+ $uploadFileName = Str::random(16) . ($suffix ? "-{$suffix}" : '') . ($extension ? ".{$extension}" : '');
+ while ($storage->exists($this->adjustPathForStorageDisk($basePath . $uploadFileName))) {
+ $uploadFileName = Str::random(3) . $uploadFileName;
+ }
+
+ $fileStream = fopen($file->getRealPath(), 'r');
+ $filePath = $basePath . $uploadFileName;
+
+ try {
+ $storage->writeStream($this->adjustPathForStorageDisk($filePath), $fileStream);
+ } catch (Exception $e) {
+ Log::error('Error when attempting file upload:' . $e->getMessage());
+
+ throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $filePath]));
+ }
+
+ return $filePath;
+ }
+
+ /**
+ * Get the storage that will be used for storing files.
+ */
+ protected function getStorageDisk(): Storage
+ {
+ return $this->fileSystem->disk($this->getStorageDiskName());
+ }
+
+ /**
+ * Get the name of the storage disk to use.
+ */
+ protected function getStorageDiskName(): string
+ {
+ $storageType = config('filesystems.attachments');
+
+ // Change to our secure-attachment disk if any of the local options
+ // are used to prevent escaping that location.
+ if ($storageType === 'local' || $storageType === 'local_secure' || $storageType === 'local_secure_restricted') {
+ $storageType = 'local_secure_attachments';
+ }
+
+ return $storageType;
+ }
+
+ /**
+ * Change the originally provided path to fit any disk-specific requirements.
+ * This also ensures the path is kept to the expected root folders.
+ */
+ protected function adjustPathForStorageDisk(string $path): string
+ {
+ $path = (new WhitespacePathNormalizer())->normalizePath(str_replace('uploads/files/', '', $path));
+
+ if ($this->getStorageDiskName() === 'local_secure_attachments') {
+ return $path;
+ }
+
+ return 'uploads/files/' . $path;
+ }
+}