visitor zipping

This commit is contained in:
Peter Papp
2021-07-29 13:01:25 +02:00
parent 0ea7447901
commit 1f1f646f62
11 changed files with 190 additions and 199 deletions
+4 -2
View File
@@ -40,7 +40,7 @@
"/chunks/files~chunks/platform~chunks/shared~chunks/shared/file-browser.js": "/chunks/files~chunks/platform~chunks/shared~chunks/shared/file-browser.js?id=8a06b7d864acff647f8c", "/chunks/files~chunks/platform~chunks/shared~chunks/shared/file-browser.js": "/chunks/files~chunks/platform~chunks/shared~chunks/shared/file-browser.js?id=8a06b7d864acff647f8c",
"/chunks/files~chunks/platform~chunks/shared~chunks/shared/file-browser~chunks/shared/single-file.js": "/chunks/files~chunks/platform~chunks/shared~chunks/shared/file-browser~chunks/shared/single-file.js?id=0de0b81edf0bb5d4617d", "/chunks/files~chunks/platform~chunks/shared~chunks/shared/file-browser~chunks/shared/single-file.js": "/chunks/files~chunks/platform~chunks/shared~chunks/shared/file-browser~chunks/shared/single-file.js?id=0de0b81edf0bb5d4617d",
"/chunks/files~chunks/settings-subscription~chunks/shared/file-browser~chunks/user-subscription.js": "/chunks/files~chunks/settings-subscription~chunks/shared/file-browser~chunks/user-subscription.js?id=c5ec9502bcfad35c502e", "/chunks/files~chunks/settings-subscription~chunks/shared/file-browser~chunks/user-subscription.js": "/chunks/files~chunks/settings-subscription~chunks/shared/file-browser~chunks/user-subscription.js?id=c5ec9502bcfad35c502e",
"/chunks/files~chunks/shared/file-browser.js": "/chunks/files~chunks/shared/file-browser.js?id=1b19035df0776555ab90", "/chunks/files~chunks/shared/file-browser.js": "/chunks/files~chunks/shared/file-browser.js?id=89464ac77e762367486b",
"/chunks/files~chunks/shared/file-browser~chunks/shared/single-file.js": "/chunks/files~chunks/shared/file-browser~chunks/shared/single-file.js?id=ad09e3f973e4db0411f1", "/chunks/files~chunks/shared/file-browser~chunks/shared/single-file.js": "/chunks/files~chunks/shared/file-browser~chunks/shared/single-file.js?id=ad09e3f973e4db0411f1",
"/chunks/forgotten-password.js": "/chunks/forgotten-password.js?id=8871529af0da8193d3a3", "/chunks/forgotten-password.js": "/chunks/forgotten-password.js?id=8871529af0da8193d3a3",
"/chunks/homepage.js": "/chunks/homepage.js?id=d29b9f09d08d673dff75", "/chunks/homepage.js": "/chunks/homepage.js?id=d29b9f09d08d673dff75",
@@ -126,5 +126,7 @@
"/chunks/platform~chunks/shared.c0f59c4d5f95687a84ad.hot-update.js": "/chunks/platform~chunks/shared.c0f59c4d5f95687a84ad.hot-update.js", "/chunks/platform~chunks/shared.c0f59c4d5f95687a84ad.hot-update.js": "/chunks/platform~chunks/shared.c0f59c4d5f95687a84ad.hot-update.js",
"/chunks/platform~chunks/shared.6db75d5fc148ae19b0b8.hot-update.js": "/chunks/platform~chunks/shared.6db75d5fc148ae19b0b8.hot-update.js", "/chunks/platform~chunks/shared.6db75d5fc148ae19b0b8.hot-update.js": "/chunks/platform~chunks/shared.6db75d5fc148ae19b0b8.hot-update.js",
"/chunks/platform~chunks/shared.a9aa2769c5345e07f90c.hot-update.js": "/chunks/platform~chunks/shared.a9aa2769c5345e07f90c.hot-update.js", "/chunks/platform~chunks/shared.a9aa2769c5345e07f90c.hot-update.js": "/chunks/platform~chunks/shared.a9aa2769c5345e07f90c.hot-update.js",
"/chunks/platform~chunks/shared.8a25cb105761d2e120e8.hot-update.js": "/chunks/platform~chunks/shared.8a25cb105761d2e120e8.hot-update.js" "/chunks/platform~chunks/shared.8a25cb105761d2e120e8.hot-update.js": "/chunks/platform~chunks/shared.8a25cb105761d2e120e8.hot-update.js",
"/chunks/files~chunks/shared/file-browser.795bed5a6b8dd1d5f361.hot-update.js": "/chunks/files~chunks/shared/file-browser.795bed5a6b8dd1d5f361.hot-update.js",
"/chunks/files~chunks/shared/file-browser.da183dcc2f390b01a554.hot-update.js": "/chunks/files~chunks/shared/file-browser.da183dcc2f390b01a554.hot-update.js"
} }
@@ -13,7 +13,7 @@
<OptionGroup v-if="item && isMultiSelectContextMenu"> <OptionGroup v-if="item && isMultiSelectContextMenu">
<Option @click.native="ItemDetail" :title="$t('context_menu.detail')" icon="detail" /> <Option @click.native="ItemDetail" :title="$t('context_menu.detail')" icon="detail" />
<Option @click.native="downloadItem" v-if="!isFolder" :title="$t('context_menu.download')" icon="download" /> <Option @click.native="downloadItem" :title="$t('context_menu.download')" icon="download" />
</OptionGroup> </OptionGroup>
<!-- Multi options --> <!-- Multi options -->
@@ -132,7 +132,7 @@
<Option @click.native="$deleteFileOrFolder(item)" :title="$t('context_menu.delete')" icon="trash" /> <Option @click.native="$deleteFileOrFolder(item)" :title="$t('context_menu.delete')" icon="trash" />
</OptionGroup> </OptionGroup>
<OptionGroup v-if="item && !isMultiSelectContextMenu && !hasFolder"> <OptionGroup v-if="item && !isMultiSelectContextMenu">
<Option @click.native="downloadItem" :title="$t('context_menu.download')" icon="download" /> <Option @click.native="downloadItem" :title="$t('context_menu.download')" icon="download" />
</OptionGroup> </OptionGroup>
</div> </div>
+3 -7
View File
@@ -1,8 +1,7 @@
<?php <?php
use Domain\Sharing\Controllers\ShareController; use Domain\Sharing\Controllers\ShareController;
use Domain\Zip\Controllers\VisitorZipFilesController; use Domain\Zip\Controllers\VisitorZipController;
use Domain\Zip\Controllers\VisitorZipFolderController;
use Domain\Files\Controllers\VisitorShowFileController; use Domain\Files\Controllers\VisitorShowFileController;
use Domain\Files\Controllers\VisitorUploadFileController; use Domain\Files\Controllers\VisitorUploadFileController;
use Domain\Folders\Controllers\VisitorCreateFolderController; use Domain\Folders\Controllers\VisitorCreateFolderController;
@@ -26,15 +25,12 @@ Route::group(['prefix' => 'editor'], function () {
}); });
// Zip shared items // Zip shared items
Route::group(['prefix' => 'zip'], function () { Route::get('/zip/{shared}', VisitorZipController::class);
Route::get('/folder/{id}/{shared}', VisitorZipFolderController::class);
Route::post('/files/{shared}', VisitorZipFilesController::class);
});
// Browse share content // Browse share content
Route::group(['prefix' => 'browse'], function () { Route::group(['prefix' => 'browse'], function () {
Route::post('/authenticate/{shared}', VisitorUnlockLockedShareController::class);
Route::get('/folders/{id}/{shared}', VisitorBrowseFolderContentController::class); Route::get('/folders/{id}/{shared}', VisitorBrowseFolderContentController::class);
Route::post('/authenticate/{shared}', VisitorUnlockLockedShareController::class);
Route::get('/navigation/{shared}', VisitorNavigationFolderTreeController::class); Route::get('/navigation/{shared}', VisitorNavigationFolderTreeController::class);
Route::get('/search/{shared}', VisitorSearchFilesAndFoldersController::class); Route::get('/search/{shared}', VisitorSearchFilesAndFoldersController::class);
Route::get('/file/{shared}', VisitorShowFileController::class); Route::get('/file/{shared}', VisitorShowFileController::class);
@@ -5,14 +5,15 @@ use Domain\Sharing\Models\Share;
class ProtectShareRecordAction class ProtectShareRecordAction
{ {
private string $message = "Sorry, you don't have permission";
public function __invoke( public function __invoke(
Share $shared Share $shared
): void { ): void {
if ($shared->is_protected) { if ($shared->is_protected) {
$abort_message = "Sorry, you don't have permission";
if (! request()->hasCookie('share_session')) { if (! request()->hasCookie('share_session')) {
abort(403, $abort_message); abort(403, $this->message);
} }
// Get shared session // Get shared session
@@ -22,12 +23,12 @@ class ProtectShareRecordAction
// Check if is requested same share record // Check if is requested same share record
if ($share_session->token !== $shared->token) { if ($share_session->token !== $shared->token) {
abort(403, $abort_message); abort(403, $this->message);
} }
// Check if share record was authenticated previously via ShareController@authenticate // Check if share record was authenticated previously via ShareController@authenticate
if (! $share_session->authenticated) { if (! $share_session->authenticated) {
abort(403, $abort_message); abort(403, $this->message);
} }
} }
} }
@@ -46,6 +46,7 @@ class SharePublicIndexController extends Controller
} }
return view('index') return view('index')
->with('status_check', [])
->with('installation', 'setup-done') ->with('installation', 'setup-done')
->with('settings', get_settings_in_json() ?? null); ->with('settings', get_settings_in_json() ?? null);
} }
@@ -0,0 +1,45 @@
<?php
namespace Domain\Zip\Actions;
use Domain\Files\Models\File;
use Domain\Folders\Models\Folder;
class GetItemsListFromUrlParamAction
{
public function __invoke(
string $user_id
): array {
$list = explode(',', request()->get('items'));
$itemList = collect($list)
->map(function ($chunk) {
$items = explode('|', $chunk);
return [
'id' => $items[0],
'type' => $items[1],
];
});
$folderIds = $itemList
->where('type', 'folder')
->pluck('id');
$fileIds = $itemList
->where('type', 'file')
->pluck('id');
$folders = Folder::whereUserId($user_id)
->whereIn('id', $folderIds)
->get();
$files = File::whereUserId($user_id)
->whereIn('id', $fileIds)
->get();
return [$folders, $files];
}
}
@@ -0,0 +1,68 @@
<?php
namespace Domain\Zip\Controllers;
use App\Http\Controllers\Controller;
use Domain\Files\Models\File;
use Domain\Sharing\Actions\ProtectShareRecordAction;
use Domain\Sharing\Actions\VerifyAccessToItemAction;
use Domain\Sharing\Models\Share;
use Domain\Traffic\Actions\RecordDownloadAction;
use Domain\Zip\Actions\GetItemsListFromUrlParamAction;
use Domain\Zip\Actions\ZipAction;
use Illuminate\Http\Request;
use ZipStream\ZipStream;
class VisitorZipController extends Controller
{
public function __construct(
public GetItemsListFromUrlParamAction $getItemsListFromUrlParam,
public ProtectShareRecordAction $protectShareRecord,
public VerifyAccessToItemAction $verifyAccessToItem,
public RecordDownloadAction $recordDownload,
public ZipAction $zip,
) {}
public function __invoke(
Request $request,
Share $shared,
): ZipStream {
// Check ability to access protected share record
($this->protectShareRecord)($shared);
list($folders, $files) = ($this->getItemsListFromUrlParam)($shared->user_id);
// Check access to requested folders
if ($folders->isNotEmpty()) {
$folders->each(
fn ($folder) => ($this->verifyAccessToItem)($folder->id, $shared)
);
}
// Check access to requested files
if ($files->isNotEmpty()) {
$file_parent_folders = File::whereUserId($shared->user_id)
->whereIn('id', $files->pluck('id'))
->get()
->pluck('folder_id')
->toArray();
// Check access to requested directory
($this->verifyAccessToItem)($file_parent_folders, $shared);
}
// Zip items
$zip = ($this->zip)($folders, $files, $shared);
($this->recordDownload)(
file_size: $zip->predictZipSize(),
user_id: $shared->user_id,
);
return $zip;
}
}
@@ -1,58 +0,0 @@
<?php
namespace Domain\Zip\Controllers;
use Illuminate\Http\Request;
use Domain\Files\Models\File;
use Domain\Sharing\Models\Share;
use App\Http\Controllers\Controller;
use Domain\Zip\Actions\ZipFilesAction;
use Domain\Traffic\Actions\RecordDownloadAction;
use Domain\Sharing\Actions\ProtectShareRecordAction;
use Domain\Sharing\Actions\VerifyAccessToItemAction;
use STS\ZipStream\ZipStream;
/**
* Guest download multiple files via zip
*/
class VisitorZipFilesController extends Controller
{
public function __construct(
private ProtectShareRecordAction $protectShareRecord,
private VerifyAccessToItemAction $verifyAccessToItem,
private RecordDownloadAction $recordDownload,
private ZipFilesAction $zipFiles,
) {
}
public function __invoke(
Request $request,
Share $shared,
): ZipStream {
// Check ability to access protected share record
($this->protectShareRecord)($shared);
$file_parent_folders = File::whereUserId($shared->user_id)
->whereIn('id', $request->items)
->get()
->pluck('folder_id')
->toArray();
// Check access to requested directory
($this->verifyAccessToItem)($file_parent_folders, $shared);
// Get requested files
$files = File::whereUserId($shared->user_id)
->whereIn('id', $request->items)
->get();
// Create zip
$zip = ($this->zipFiles)($files, $shared);
($this->recordDownload)(
file_size: $zip->predictZipSize(),
user_id: $shared->user_id,
);
return $zip;
}
}
@@ -1,54 +0,0 @@
<?php
namespace Domain\Zip\Controllers;
use STS\ZipStream\ZipStream;
use Domain\Sharing\Models\Share;
use Domain\Folders\Models\Folder;
use App\Http\Controllers\Controller;
use Domain\Zip\Actions\ZipFolderAction;
use Domain\Traffic\Actions\RecordDownloadAction;
use Domain\Sharing\Actions\ProtectShareRecordAction;
use Domain\Sharing\Actions\VerifyAccessToItemAction;
/**
* Guest download folder via zip
*/
class VisitorZipFolderController extends Controller
{
public function __construct(
private ProtectShareRecordAction $protectShareRecord,
private VerifyAccessToItemAction $verifyAccessToItem,
private RecordDownloadAction $recordDownload,
private ZipFolderAction $zipFolder,
) {
}
public function __invoke(
string $id,
Share $shared,
): ZipStream {
// Check ability to access protected share record
($this->protectShareRecord)($shared);
// Check access to requested folder
($this->verifyAccessToItem)($id, $shared);
// Get folder
$folder = Folder::whereUserId($shared->user_id)
->where('id', $id);
if (! $folder->exists()) {
abort(404, "Requested folder doesn't exists.");
}
// Create zip
$zip = ($this->zipFolder)($id, $shared);
($this->recordDownload)(
file_size: $zip->predictZipSize(),
user_id: $shared->user_id,
);
return $zip;
}
}
+3 -29
View File
@@ -5,9 +5,8 @@ namespace Domain\Zip\Controllers;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Domain\Files\Models\File;
use Domain\Folders\Models\Folder;
use Domain\Traffic\Actions\RecordDownloadAction; use Domain\Traffic\Actions\RecordDownloadAction;
use Domain\Zip\Actions\GetItemsListFromUrlParamAction;
use Domain\Zip\Actions\ZipAction; use Domain\Zip\Actions\ZipAction;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@@ -18,40 +17,15 @@ class ZipController extends Controller
public function __construct( public function __construct(
public ZipAction $zip, public ZipAction $zip,
public RecordDownloadAction $recordDownload, public RecordDownloadAction $recordDownload,
public GetItemsListFromUrlParamAction $getItemsListFromUrlParam,
) {} ) {}
public function __invoke( public function __invoke(
Request $request, Request $request,
): ZipStream { ): ZipStream {
$user_id = Auth::id(); $user_id = Auth::id();
$items = explode(',', $request->get('items'));
$itemList = collect($items) list($folders, $files) = ($this->getItemsListFromUrlParam)($user_id);
->map(function ($chunk) {
$items = explode('|', $chunk);
return [
'id' => $items[0],
'type' => $items[1],
];
});
$folderIds = $itemList
->where('type', 'folder')
->pluck('id');
$fileIds = $itemList
->where('type', 'file')
->pluck('id');
$folders = Folder::whereUserId($user_id)
->whereIn('id', $folderIds)
->get();
$files = File::whereUserId($user_id)
->whereIn('id', $fileIds)
->get();
$zip = ($this->zip)($folders, $files); $zip = ($this->zip)($folders, $files);
+54 -38
View File
@@ -1,10 +1,10 @@
<?php <?php
namespace Tests\Domain\Zip; namespace Tests\Domain\Zip;
use Storage; use Storage;
use Tests\TestCase; use Tests\TestCase;
use App\Users\Models\User; use App\Users\Models\User;
use Domain\Zip\Models\Zip;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Domain\Files\Models\File; use Domain\Files\Models\File;
use Domain\Sharing\Models\Share; use Domain\Sharing\Models\Share;
@@ -24,15 +24,21 @@ class SharedZippingTest extends TestCase
$user = User::factory(User::class) $user = User::factory(User::class)
->create(); ->create();
$sharedFolder = Folder::factory(Folder::class)
->create([
'user_id' => $user->id,
]);
$folder = Folder::factory(Folder::class) $folder = Folder::factory(Folder::class)
->create([ ->create([
'user_id' => $user->id, 'user_id' => $user->id,
'parent_id' => $sharedFolder->id,
]); ]);
collect([0, 1]) collect([0, 1])
->each(function ($index) use ($folder, $user) { ->each(function ($index) use ($folder, $user) {
$file = UploadedFile::fake() $file = UploadedFile::fake()
->create(Str::random() . "-fake-file-$index.pdf", 1000, 'application/pdf'); ->create("fake-inner-file-$index.pdf", 1200, 'application/pdf');
Storage::putFileAs("files/$user->id", $file, $file->name); Storage::putFileAs("files/$user->id", $file, $file->name);
@@ -46,14 +52,35 @@ class SharedZippingTest extends TestCase
]); ]);
}); });
collect([0, 1])
->each(function ($index) use ($sharedFolder, $user) {
$file = UploadedFile::fake()
->create(Str::random() . "-fake-file-$index.pdf", 1000, 'application/pdf');
Storage::putFileAs("files/$user->id", $file, $file->name);
File::factory(File::class)
->create([
'filesize' => $file->getSize(),
'folder_id' => $sharedFolder->id,
'user_id' => $user->id,
'basename' => $file->name,
'name' => "fake-file-$index.pdf",
]);
});
$share = Share::factory(Share::class) $share = Share::factory(Share::class)
->create([ ->create([
'item_id' => $folder->id, 'item_id' => $sharedFolder->id,
'user_id' => $user->id, 'user_id' => $user->id,
'type' => 'folder', 'type' => 'folder',
'is_protected' => $is_protected, 'is_protected' => $is_protected,
]); ]);
$files = File::all()
->pluck('id')
->toArray();
// Check shared item protected by password // Check shared item protected by password
if ($is_protected) { if ($is_protected) {
$cookie = ['share_session' => json_encode([ $cookie = ['share_session' => json_encode([
@@ -63,24 +90,18 @@ class SharedZippingTest extends TestCase
$this $this
->withUnencryptedCookies($cookie) ->withUnencryptedCookies($cookie)
->post("/api/zip/files/$share->token", [ ->get("/api/zip/{$share->token}?items=$files[0]|file,$files[1]|file,$folder->id|folder")
'items' => File::all()->pluck('id'), ->assertStatus(200)
])->assertStatus(201); ->assertHeader('content-type', 'application/x-zip');
} }
// Check public shared item // Check public shared item
if (!$is_protected) { if (!$is_protected) {
$this->postJson("/api/zip/files/$share->token", [ $this
'items' => File::all()->pluck('id'), ->get("/api/zip/{$share->token}?items=$files[0]|file,$files[1]|file,$folder->id|folder")
])->assertStatus(201); ->assertStatus(200)
->assertHeader('content-type', 'application/x-zip');
} }
$this->assertDatabaseHas('zips', [
'user_id' => $user->id,
'shared_token' => $share->token,
]);
Storage::assertExists('zip/' . Zip::first()->basename);
}); });
} }
@@ -119,6 +140,10 @@ class SharedZippingTest extends TestCase
'is_protected' => $is_protected, 'is_protected' => $is_protected,
]); ]);
$files = File::all()
->pluck('id')
->toArray();
// Check shared item protected by password // Check shared item protected by password
if ($is_protected) { if ($is_protected) {
$cookie = ['share_session' => json_encode([ $cookie = ['share_session' => json_encode([
@@ -128,17 +153,18 @@ class SharedZippingTest extends TestCase
$this $this
->withUnencryptedCookies($cookie) ->withUnencryptedCookies($cookie)
->post("/api/zip/files/$share->token", [ ->get("/api/zip/$share->token?items=$files[0]|file,$files[1]|file")
'items' => File::all()->pluck('id'), ->assertStatus(403);
])->assertStatus(403);
} }
// Check public shared item // Check public shared item
if (!$is_protected) { if (!$is_protected) {
$this->postJson("/api/zip/files/$share->token", [ $this
'items' => File::all()->pluck('id'), ->get("/api/zip/$share->token?items=$files[0]|file,$files[1]|file")
])->assertStatus(403); ->assertStatus(403);
} }
File::all()->each(fn ($file) => $file->delete());
}); });
} }
@@ -198,25 +224,15 @@ class SharedZippingTest extends TestCase
$this $this
->withUnencryptedCookies($cookie) ->withUnencryptedCookies($cookie)
->get("/api/zip/folder/$children->id/$share->token") ->get("/api/zip/$share->token?items=$children->id|folder")
->assertStatus(201); ->assertStatus(200);
} }
// Check public shared item // Check public shared item
if (!$is_protected) { if (!$is_protected) {
$this->getJson("/api/zip/folder/$children->id/$share->token") $this->getJson("/api/zip/$share->token?items=$children->id|folder")
->assertStatus(201); ->assertStatus(200);
} }
$this->assertDatabaseHas('zips', [
'user_id' => $user->id,
'shared_token' => $share->token,
]);
Zip::all()
->each(function ($zip) {
Storage::assertExists("zip/$zip->basename");
});
}); });
} }
@@ -252,13 +268,13 @@ class SharedZippingTest extends TestCase
$this $this
->withUnencryptedCookies($cookie) ->withUnencryptedCookies($cookie)
->get("/api/zip/folder/$folder->id/$share->token") ->get("/api/zip/$share->token?items=$folder->id|folder")
->assertStatus(403); ->assertStatus(403);
} }
// Check public shared item // Check public shared item
if (!$is_protected) { if (!$is_protected) {
$this->getJson("/api/zip/folder/$folder->id/$share->token") $this->getJson("/api/zip/$share->token?items=$folder->id|folder")
->assertStatus(403); ->assertStatus(403);
} }
}); });