Skip to content

Commit 8ea3855

Browse files
committed
ZIP Import: Added upload handling
Split attachment service storage work out so it can be shared.
1 parent 74fce96 commit 8ea3855

File tree

5 files changed

+195
-103
lines changed

5 files changed

+195
-103
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace BookStack\Exceptions;
4+
5+
class ZipValidationException extends \Exception
6+
{
7+
public function __construct(
8+
public array $errors
9+
) {
10+
parent::__construct();
11+
}
12+
}

app/Exports/Controllers/ImportController.php

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@
22

33
namespace BookStack\Exports\Controllers;
44

5-
use BookStack\Exports\Import;
6-
use BookStack\Exports\ZipExports\ZipExportReader;
7-
use BookStack\Exports\ZipExports\ZipExportValidator;
5+
use BookStack\Exceptions\ZipValidationException;
6+
use BookStack\Exports\ImportRepo;
87
use BookStack\Http\Controller;
8+
use BookStack\Uploads\AttachmentService;
99
use Illuminate\Http\Request;
1010

1111
class ImportController extends Controller
1212
{
13-
public function __construct()
14-
{
13+
public function __construct(
14+
protected ImportRepo $imports,
15+
) {
1516
$this->middleware('can:content-import');
1617
}
1718

@@ -27,35 +28,17 @@ public function start(Request $request)
2728
public function upload(Request $request)
2829
{
2930
$this->validate($request, [
30-
'file' => ['required', 'file']
31+
'file' => ['required', ...AttachmentService::getFileValidationRules()]
3132
]);
3233

3334
$file = $request->file('file');
34-
$zipPath = $file->getRealPath();
35-
36-
$errors = (new ZipExportValidator($zipPath))->validate();
37-
if ($errors) {
38-
session()->flash('validation_errors', $errors);
35+
try {
36+
$import = $this->imports->storeFromUpload($file);
37+
} catch (ZipValidationException $exception) {
38+
session()->flash('validation_errors', $exception->errors);
3939
return redirect('/import');
4040
}
4141

42-
$zipEntityInfo = (new ZipExportReader($zipPath))->getEntityInfo();
43-
$import = new Import();
44-
$import->name = $zipEntityInfo['name'];
45-
$import->book_count = $zipEntityInfo['book_count'];
46-
$import->chapter_count = $zipEntityInfo['chapter_count'];
47-
$import->page_count = $zipEntityInfo['page_count'];
48-
$import->created_by = user()->id;
49-
$import->size = filesize($zipPath);
50-
// TODO - Set path
51-
// TODO - Save
52-
53-
// TODO - Split out attachment service to separate out core filesystem/disk stuff
54-
// To reuse for import handling
55-
56-
dd('passed');
57-
// TODO - Upload to storage
58-
// TODO - Store info/results for display:
59-
// TODO - Send user to next import stage
42+
return redirect("imports/{$import->id}");
6043
}
6144
}

app/Exports/ImportRepo.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace BookStack\Exports;
4+
5+
use BookStack\Exceptions\ZipValidationException;
6+
use BookStack\Exports\ZipExports\ZipExportReader;
7+
use BookStack\Exports\ZipExports\ZipExportValidator;
8+
use BookStack\Uploads\FileStorage;
9+
use Symfony\Component\HttpFoundation\File\UploadedFile;
10+
11+
class ImportRepo
12+
{
13+
public function __construct(
14+
protected FileStorage $storage,
15+
) {
16+
}
17+
18+
public function storeFromUpload(UploadedFile $file): Import
19+
{
20+
$zipPath = $file->getRealPath();
21+
22+
$errors = (new ZipExportValidator($zipPath))->validate();
23+
if ($errors) {
24+
throw new ZipValidationException($errors);
25+
}
26+
27+
$zipEntityInfo = (new ZipExportReader($zipPath))->getEntityInfo();
28+
$import = new Import();
29+
$import->name = $zipEntityInfo['name'];
30+
$import->book_count = $zipEntityInfo['book_count'];
31+
$import->chapter_count = $zipEntityInfo['chapter_count'];
32+
$import->page_count = $zipEntityInfo['page_count'];
33+
$import->created_by = user()->id;
34+
$import->size = filesize($zipPath);
35+
36+
$path = $this->storage->uploadFile(
37+
$file,
38+
'uploads/files/imports/',
39+
'',
40+
'zip'
41+
);
42+
43+
$import->path = $path;
44+
$import->save();
45+
46+
return $import;
47+
}
48+
}

app/Uploads/AttachmentService.php

Lines changed: 12 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -4,75 +4,31 @@
44

55
use BookStack\Exceptions\FileUploadException;
66
use Exception;
7-
use Illuminate\Contracts\Filesystem\Filesystem as Storage;
8-
use Illuminate\Filesystem\FilesystemManager;
9-
use Illuminate\Support\Facades\Log;
10-
use Illuminate\Support\Str;
11-
use League\Flysystem\WhitespacePathNormalizer;
127
use Symfony\Component\HttpFoundation\File\UploadedFile;
138

149
class AttachmentService
1510
{
1611
public function __construct(
17-
protected FilesystemManager $fileSystem
12+
protected FileStorage $storage,
1813
) {
1914
}
2015

21-
/**
22-
* Get the storage that will be used for storing files.
23-
*/
24-
protected function getStorageDisk(): Storage
25-
{
26-
return $this->fileSystem->disk($this->getStorageDiskName());
27-
}
28-
29-
/**
30-
* Get the name of the storage disk to use.
31-
*/
32-
protected function getStorageDiskName(): string
33-
{
34-
$storageType = config('filesystems.attachments');
35-
36-
// Change to our secure-attachment disk if any of the local options
37-
// are used to prevent escaping that location.
38-
if ($storageType === 'local' || $storageType === 'local_secure' || $storageType === 'local_secure_restricted') {
39-
$storageType = 'local_secure_attachments';
40-
}
41-
42-
return $storageType;
43-
}
44-
45-
/**
46-
* Change the originally provided path to fit any disk-specific requirements.
47-
* This also ensures the path is kept to the expected root folders.
48-
*/
49-
protected function adjustPathForStorageDisk(string $path): string
50-
{
51-
$path = (new WhitespacePathNormalizer())->normalizePath(str_replace('uploads/files/', '', $path));
52-
53-
if ($this->getStorageDiskName() === 'local_secure_attachments') {
54-
return $path;
55-
}
56-
57-
return 'uploads/files/' . $path;
58-
}
59-
6016
/**
6117
* Stream an attachment from storage.
6218
*
6319
* @return resource|null
6420
*/
6521
public function streamAttachmentFromStorage(Attachment $attachment)
6622
{
67-
return $this->getStorageDisk()->readStream($this->adjustPathForStorageDisk($attachment->path));
23+
return $this->storage->getReadStream($attachment->path);
6824
}
6925

7026
/**
7127
* Read the file size of an attachment from storage, in bytes.
7228
*/
7329
public function getAttachmentFileSize(Attachment $attachment): int
7430
{
75-
return $this->getStorageDisk()->size($this->adjustPathForStorageDisk($attachment->path));
31+
return $this->storage->getSize($attachment->path);
7632
}
7733

7834
/**
@@ -195,15 +151,9 @@ public function deleteFile(Attachment $attachment)
195151
* Delete a file from the filesystem it sits on.
196152
* Cleans any empty leftover folders.
197153
*/
198-
protected function deleteFileInStorage(Attachment $attachment)
154+
protected function deleteFileInStorage(Attachment $attachment): void
199155
{
200-
$storage = $this->getStorageDisk();
201-
$dirPath = $this->adjustPathForStorageDisk(dirname($attachment->path));
202-
203-
$storage->delete($this->adjustPathForStorageDisk($attachment->path));
204-
if (count($storage->allFiles($dirPath)) === 0) {
205-
$storage->deleteDirectory($dirPath);
206-
}
156+
$this->storage->delete($attachment->path);
207157
}
208158

209159
/**
@@ -213,32 +163,20 @@ protected function deleteFileInStorage(Attachment $attachment)
213163
*/
214164
protected function putFileInStorage(UploadedFile $uploadedFile): string
215165
{
216-
$storage = $this->getStorageDisk();
217166
$basePath = 'uploads/files/' . date('Y-m-M') . '/';
218167

219-
$uploadFileName = Str::random(16) . '-' . $uploadedFile->getClientOriginalExtension();
220-
while ($storage->exists($this->adjustPathForStorageDisk($basePath . $uploadFileName))) {
221-
$uploadFileName = Str::random(3) . $uploadFileName;
222-
}
223-
224-
$attachmentStream = fopen($uploadedFile->getRealPath(), 'r');
225-
$attachmentPath = $basePath . $uploadFileName;
226-
227-
try {
228-
$storage->writeStream($this->adjustPathForStorageDisk($attachmentPath), $attachmentStream);
229-
} catch (Exception $e) {
230-
Log::error('Error when attempting file upload:' . $e->getMessage());
231-
232-
throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentPath]));
233-
}
234-
235-
return $attachmentPath;
168+
return $this->storage->uploadFile(
169+
$uploadedFile,
170+
$basePath,
171+
$uploadedFile->getClientOriginalExtension(),
172+
''
173+
);
236174
}
237175

238176
/**
239177
* Get the file validation rules for attachments.
240178
*/
241-
public function getFileValidationRules(): array
179+
public static function getFileValidationRules(): array
242180
{
243181
return ['file', 'max:' . (config('app.upload_limit') * 1000)];
244182
}

app/Uploads/FileStorage.php

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
namespace BookStack\Uploads;
4+
5+
use BookStack\Exceptions\FileUploadException;
6+
use Exception;
7+
use Illuminate\Contracts\Filesystem\Filesystem as Storage;
8+
use Illuminate\Filesystem\FilesystemManager;
9+
use Illuminate\Support\Facades\Log;
10+
use Illuminate\Support\Str;
11+
use League\Flysystem\WhitespacePathNormalizer;
12+
use Symfony\Component\HttpFoundation\File\UploadedFile;
13+
14+
class FileStorage
15+
{
16+
public function __construct(
17+
protected FilesystemManager $fileSystem,
18+
) {
19+
}
20+
21+
/**
22+
* @return resource|null
23+
*/
24+
public function getReadStream(string $path)
25+
{
26+
return $this->getStorageDisk()->readStream($this->adjustPathForStorageDisk($path));
27+
}
28+
29+
public function getSize(string $path): int
30+
{
31+
return $this->getStorageDisk()->size($this->adjustPathForStorageDisk($path));
32+
}
33+
34+
public function delete(string $path, bool $removeEmptyDir = false): void
35+
{
36+
$storage = $this->getStorageDisk();
37+
$adjustedPath = $this->adjustPathForStorageDisk($path);
38+
$dir = dirname($adjustedPath);
39+
40+
$storage->delete($adjustedPath);
41+
if ($removeEmptyDir && count($storage->allFiles($dir)) === 0) {
42+
$storage->deleteDirectory($dir);
43+
}
44+
}
45+
46+
/**
47+
* @throws FileUploadException
48+
*/
49+
public function uploadFile(UploadedFile $file, string $subDirectory, string $suffix, string $extension): string
50+
{
51+
$storage = $this->getStorageDisk();
52+
$basePath = trim($subDirectory, '/') . '/';
53+
54+
$uploadFileName = Str::random(16) . ($suffix ? "-{$suffix}" : '') . ($extension ? ".{$extension}" : '');
55+
while ($storage->exists($this->adjustPathForStorageDisk($basePath . $uploadFileName))) {
56+
$uploadFileName = Str::random(3) . $uploadFileName;
57+
}
58+
59+
$fileStream = fopen($file->getRealPath(), 'r');
60+
$filePath = $basePath . $uploadFileName;
61+
62+
try {
63+
$storage->writeStream($this->adjustPathForStorageDisk($filePath), $fileStream);
64+
} catch (Exception $e) {
65+
Log::error('Error when attempting file upload:' . $e->getMessage());
66+
67+
throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $filePath]));
68+
}
69+
70+
return $filePath;
71+
}
72+
73+
/**
74+
* Get the storage that will be used for storing files.
75+
*/
76+
protected function getStorageDisk(): Storage
77+
{
78+
return $this->fileSystem->disk($this->getStorageDiskName());
79+
}
80+
81+
/**
82+
* Get the name of the storage disk to use.
83+
*/
84+
protected function getStorageDiskName(): string
85+
{
86+
$storageType = config('filesystems.attachments');
87+
88+
// Change to our secure-attachment disk if any of the local options
89+
// are used to prevent escaping that location.
90+
if ($storageType === 'local' || $storageType === 'local_secure' || $storageType === 'local_secure_restricted') {
91+
$storageType = 'local_secure_attachments';
92+
}
93+
94+
return $storageType;
95+
}
96+
97+
/**
98+
* Change the originally provided path to fit any disk-specific requirements.
99+
* This also ensures the path is kept to the expected root folders.
100+
*/
101+
protected function adjustPathForStorageDisk(string $path): string
102+
{
103+
$path = (new WhitespacePathNormalizer())->normalizePath(str_replace('uploads/files/', '', $path));
104+
105+
if ($this->getStorageDiskName() === 'local_secure_attachments') {
106+
return $path;
107+
}
108+
109+
return 'uploads/files/' . $path;
110+
}
111+
}

0 commit comments

Comments
 (0)