mirror of
https://github.com/projectsend/projectsend.git
synced 2026-10-03 12:54:18 +00:00
Folders in the API: list, read, create, rename, move, delete and share
An integration could put a file into a folder by id but could not see, make or arrange the folders themselves, so mirroring a directory tree into ProjectSend was impossible over the API. The hosted AI connector already creates, lists and shares folders. GET /folders polls like every list (updated_since, cursor) and filters on parent_id, top_level and search. Each folder carries its ancestors and a display path, trimmed for a client-scoped token to the folders it may see (BreadcrumbBuilder::visible's rule), worked out for a whole page in two queries by FolderTrails. POST /folders returns an existing folder of the same name in the same place with a 200 rather than making a second one, so a retried request is safe. PATCH renames and moves. DELETE refuses a non-empty folder with 409 unless content_action=cascade_delete is sent, and then asks UndeletableFiles exactly as the web does. Sharing goes through FolderSharing. Every write uses the web's policy, scope and FolderService, and asks Folder::uploadableBy for every parent it writes, creation included. Public state stays web-only: the resource reports `public`, nothing here changes it. A file's `folder` now carries `parent_id` as well.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Files\Folders;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Modules\Files\Access\StaffLibraryScope;
|
||||
use App\Modules\Files\Models\Folder;
|
||||
|
||||
/**
|
||||
* The ancestors of a whole page of folders, in two queries however long the
|
||||
* page is, trimmed to what the viewer may see.
|
||||
*
|
||||
* BreadcrumbBuilder answers this for one folder on a screen. A list
|
||||
* endpoint needs it for every row, and a query per row is the cost a
|
||||
* listing must not have.
|
||||
*
|
||||
* Trimmed the way BreadcrumbBuilder::visible() trims the client portal's
|
||||
* trail: the list starts at the first ancestor the viewer can reach, since
|
||||
* a client-scoped staff member holding a client's folder deep in somebody
|
||||
* else's tree has no business reading the names of the folders above it.
|
||||
* An unscoped staff member reaches every folder, so for them nothing is
|
||||
* ever trimmed.
|
||||
*/
|
||||
class FolderTrails
|
||||
{
|
||||
public function __construct(
|
||||
private readonly StaffLibraryScope $scope,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param iterable<Folder> $folders
|
||||
* @return array<int, list<array{id: int, name: string}>> folder id => its visible ancestors, root first, itself excluded
|
||||
*/
|
||||
public function ancestors(iterable $folders, User $viewer): array
|
||||
{
|
||||
$chains = [];
|
||||
$allIds = [];
|
||||
|
||||
foreach ($folders as $folder) {
|
||||
$ids = $folder->ancestorIds();
|
||||
$chains[$folder->id] = $ids;
|
||||
array_push($allIds, ...$ids);
|
||||
}
|
||||
|
||||
$allIds = array_values(array_unique($allIds));
|
||||
|
||||
if ($allIds === []) {
|
||||
return array_map(fn (): array => [], $chains);
|
||||
}
|
||||
|
||||
$names = Folder::query()->whereIn('id', $allIds)->pluck('name', 'id')->all();
|
||||
|
||||
$visible = $viewer->isClientScoped()
|
||||
? array_flip($this->scope->folders($viewer)->whereIn('folders.id', $allIds)->pluck('folders.id')->all())
|
||||
: array_flip($allIds);
|
||||
|
||||
$out = [];
|
||||
|
||||
foreach ($chains as $folderId => $ids) {
|
||||
$trail = [];
|
||||
$reached = false;
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$reached = $reached || isset($visible[$id]);
|
||||
|
||||
if ($reached && isset($names[$id])) {
|
||||
$trail[] = ['id' => $id, 'name' => (string) $names[$id]];
|
||||
}
|
||||
}
|
||||
|
||||
$out[$folderId] = $trail;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Files\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Files\Access\StaffLibraryScope;
|
||||
use App\Modules\Files\Folders\FolderTrails;
|
||||
use App\Modules\Files\Http\Controllers\Concerns\ResolvesShareTargets;
|
||||
use App\Modules\Files\Http\Resources\Api\FolderResource;
|
||||
use App\Modules\Files\Models\Folder;
|
||||
use App\Modules\Files\Sharing\FolderSharing;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
/**
|
||||
* Sharing a folder with a client or a group: `{type: client|group, id}`.
|
||||
*
|
||||
* A client a folder is shared with sees everything inside it, including
|
||||
* folders and files added later.
|
||||
*
|
||||
* Both the target resolution (ResolvesShareTargets) and the effects
|
||||
* (FolderSharing — the row, the activity entry, the in-app notification,
|
||||
* the digest email) are shared with the web controller, so the two surfaces
|
||||
* cannot drift. "May share" is "may edit", as on the web.
|
||||
*/
|
||||
class FolderAssignmentsController extends Controller
|
||||
{
|
||||
use ResolvesShareTargets;
|
||||
|
||||
public function __construct(
|
||||
private readonly StaffLibraryScope $scope,
|
||||
private readonly FolderSharing $sharing,
|
||||
private readonly FolderTrails $trails,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Share a folder.
|
||||
*
|
||||
* Sharing it again with the same client or group leaves one share.
|
||||
*/
|
||||
public function store(Request $request, Folder $folder): FolderResource
|
||||
{
|
||||
Gate::authorize('update', $folder);
|
||||
|
||||
[$assignable, $targetName] = $this->resolveRequestedTarget(
|
||||
$request,
|
||||
__('Folders can only be shared with clients or groups.'),
|
||||
);
|
||||
|
||||
$this->sharing->assign($folder, $assignable, $targetName);
|
||||
|
||||
return $this->resource($request, $folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop sharing a folder.
|
||||
*/
|
||||
public function destroy(Request $request, Folder $folder): FolderResource
|
||||
{
|
||||
Gate::authorize('update', $folder);
|
||||
|
||||
[$assignable, $targetName] = $this->resolveRequestedTarget(
|
||||
$request,
|
||||
__('Folders can only be shared with clients or groups.'),
|
||||
);
|
||||
|
||||
$this->sharing->unassign($folder, $assignable, $targetName);
|
||||
|
||||
return $this->resource($request, $folder);
|
||||
}
|
||||
|
||||
private function resource(Request $request, Folder $folder): FolderResource
|
||||
{
|
||||
$folder = $folder->fresh() ?? $folder;
|
||||
$folder->load('assignments.assignable');
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
if ($user !== null) {
|
||||
$folder->setRelation('trail', collect($this->trails->ancestors([$folder], $user)[$folder->id] ?? []));
|
||||
}
|
||||
|
||||
return new FolderResource($folder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Files\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Modules\Api\Support\PollingQuery;
|
||||
use App\Modules\Audit\Action;
|
||||
use App\Modules\Audit\ActivityLogger;
|
||||
use App\Modules\Files\Access\StaffLibraryScope;
|
||||
use App\Modules\Files\Folders\FolderService;
|
||||
use App\Modules\Files\Folders\FolderTrails;
|
||||
use App\Modules\Files\Folders\UndeletableFiles;
|
||||
use App\Modules\Files\Http\Resources\Api\FolderResource;
|
||||
use App\Modules\Files\Models\File;
|
||||
use App\Modules\Files\Models\Folder;
|
||||
use App\Support\Rules;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* The staff library's folders.
|
||||
*
|
||||
* Which folders a token sees is the same question the library screen
|
||||
* answers, so a staff member limited to their assigned clients gets exactly
|
||||
* the folders they see on the web. Every write goes through the same
|
||||
* service, policy and placement rule as the web screen.
|
||||
*/
|
||||
class FoldersController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly StaffLibraryScope $scope,
|
||||
private readonly PollingQuery $polling,
|
||||
private readonly FolderService $folders,
|
||||
private readonly FolderTrails $trails,
|
||||
private readonly UndeletableFiles $undeletable,
|
||||
private readonly ActivityLogger $activity,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* List folders.
|
||||
*
|
||||
* Cursor paginated, like every list. Pass `updated_since` to poll for
|
||||
* folders created, renamed or moved since a point in time. `parent_id`
|
||||
* lists the folders directly inside one folder, and `top_level=1` the
|
||||
* folders at the top of the library.
|
||||
*
|
||||
* Moving a folder updates the folder itself and every folder under it,
|
||||
* so a poll sees the whole moved subtree.
|
||||
*/
|
||||
public function index(Request $request): AnonymousResourceCollection
|
||||
{
|
||||
$user = $request->user();
|
||||
assert($user !== null);
|
||||
|
||||
$filters = $request->validate($this->polling->rules() + [
|
||||
'parent_id' => ['nullable', 'integer'],
|
||||
'top_level' => ['nullable', 'boolean'],
|
||||
'search' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$query = $this->scope->folders($user);
|
||||
|
||||
if (($filters['parent_id'] ?? null) !== null) {
|
||||
$query->where('folders.parent_id', (int) $filters['parent_id']);
|
||||
}
|
||||
|
||||
if ($request->boolean('top_level')) {
|
||||
$query->whereNull('folders.parent_id');
|
||||
}
|
||||
|
||||
if (($filters['search'] ?? null) !== null) {
|
||||
$query->where('folders.name', 'like', '%'.$filters['search'].'%');
|
||||
}
|
||||
|
||||
$page = $this->polling->paginate($request, $query, 'folders');
|
||||
|
||||
/** @var Collection<int, Folder> $items */
|
||||
$items = collect($page->items());
|
||||
$this->attachTrails($items, $user);
|
||||
|
||||
return FolderResource::collection($page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a folder, with the clients and groups it is shared with.
|
||||
*/
|
||||
public function show(Request $request, Folder $folder): FolderResource
|
||||
{
|
||||
Gate::authorize('view', $folder);
|
||||
|
||||
return $this->resource($folder, $request->user());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a folder.
|
||||
*
|
||||
* At the top of the library, or inside `parent_id`. Requires the
|
||||
* `create_own_folders` ability, and `upload` with it.
|
||||
*
|
||||
* If a folder with the same name already exists in the same place, that
|
||||
* folder is returned with a 200 instead of a second one being made, so
|
||||
* retrying a request is safe. A new folder answers 201.
|
||||
*/
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
assert($user !== null);
|
||||
|
||||
// The same pair FoldersController::store asks on the web: a folder
|
||||
// nobody can put anything into is no use.
|
||||
abort_unless($user->can('create_own_folders') && $user->can('upload'), 403);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'parent_id' => Rules::folderId(),
|
||||
]);
|
||||
|
||||
$parent = $this->resolveParent($user, $validated['parent_id'] ?? null);
|
||||
|
||||
// A folder inside a public one is public, so creating one there is
|
||||
// placing content into it (Folder::uploadableBy).
|
||||
abort_unless(Folder::uploadableBy($user, $parent), 403);
|
||||
|
||||
$existing = $this->scope->folders($user)
|
||||
->where('folders.parent_id', $parent?->id)
|
||||
->where('folders.name', $validated['name'])
|
||||
->orderBy('folders.id')
|
||||
->first();
|
||||
|
||||
if ($existing instanceof Folder) {
|
||||
return $this->resource($existing, $user)->response()->setStatusCode(200);
|
||||
}
|
||||
|
||||
$folder = $this->folders->create($validated['name'], $parent);
|
||||
|
||||
$this->activity->log(Action::FolderCreated, subject: $folder);
|
||||
|
||||
return $this->resource($folder, $user)->response()->setStatusCode(201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename or move a folder.
|
||||
*
|
||||
* Only the fields you send change. `parent_id: null` moves the folder to
|
||||
* the top of the library. A folder moves with everything inside it, and
|
||||
* cannot be moved into itself or one of its own subfolders.
|
||||
*/
|
||||
public function update(Request $request, Folder $folder): FolderResource
|
||||
{
|
||||
$user = $request->user();
|
||||
assert($user !== null);
|
||||
|
||||
Gate::authorize('update', $folder);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['sometimes', 'required', 'string', 'max:255'],
|
||||
'parent_id' => ['sometimes', ...Rules::folderId()],
|
||||
]);
|
||||
|
||||
if (array_key_exists('name', $validated) && $validated['name'] !== $folder->name) {
|
||||
$folder->update(['name' => $validated['name']]);
|
||||
$this->activity->log(Action::FolderRenamed, subject: $folder);
|
||||
}
|
||||
|
||||
if (array_key_exists('parent_id', $validated)) {
|
||||
$newParentId = $validated['parent_id'] === null ? null : (int) $validated['parent_id'];
|
||||
|
||||
if ($newParentId !== $folder->parent_id) {
|
||||
$newParent = $this->resolveParent($user, $newParentId);
|
||||
|
||||
// Dropping a folder into a public parent publishes its whole
|
||||
// subtree, the act FoldersController::move refuses without
|
||||
// `upload_public` (GHSA-rxf8-wh8v-jm9j).
|
||||
abort_unless(Folder::uploadableBy($user, $newParent), 403);
|
||||
|
||||
$this->folders->move($folder, $newParent);
|
||||
$this->activity->log(Action::FolderMoved, subject: $folder);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->resource($folder->fresh() ?? $folder, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a folder.
|
||||
*
|
||||
* An empty folder is deleted straight away. A folder holding files or
|
||||
* other folders answers 409 unless you send
|
||||
* `content_action=cascade_delete`, which deletes the folder, every folder
|
||||
* under it and every file inside them, as the web screen does. There is
|
||||
* no restore.
|
||||
*
|
||||
* A cascade is refused with 403 if the folder holds any file this token
|
||||
* may not delete itself.
|
||||
*/
|
||||
public function destroy(Request $request, Folder $folder): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
assert($user !== null);
|
||||
|
||||
Gate::authorize('delete', $folder);
|
||||
|
||||
$validated = $request->validate([
|
||||
'content_action' => ['nullable', Rule::in(['cascade_delete'])],
|
||||
]);
|
||||
|
||||
$subtree = $folder->subtreeFolderIds();
|
||||
$hasContent = count($subtree) > 1
|
||||
|| File::query()->whereIn('folder_id', $subtree)->exists();
|
||||
|
||||
// A sync job with a bug in it must not be one request away from
|
||||
// emptying a client's folder: the cascade has to be asked for.
|
||||
abort_if(
|
||||
$hasContent && ($validated['content_action'] ?? null) !== 'cascade_delete',
|
||||
409,
|
||||
__('This folder is not empty. Send content_action=cascade_delete to delete it with everything inside it.'),
|
||||
);
|
||||
|
||||
$blocked = $this->undeletable->count($user, $folder);
|
||||
|
||||
abort_if($blocked > 0, 403, trans_choice(
|
||||
'This folder cannot be deleted: it holds :count file you may not delete.|This folder cannot be deleted: it holds :count files you may not delete.',
|
||||
$blocked,
|
||||
['count' => (string) $blocked],
|
||||
));
|
||||
|
||||
$name = $folder->name;
|
||||
|
||||
$this->folders->delete($folder);
|
||||
|
||||
$this->activity->log(Action::FolderDeleted, context: ['name' => $name]);
|
||||
|
||||
return response()->json(status: 204);
|
||||
}
|
||||
|
||||
private function resource(Folder $folder, ?User $user): FolderResource
|
||||
{
|
||||
$folder->load('assignments.assignable');
|
||||
|
||||
if ($user !== null) {
|
||||
$this->attachTrails(collect([$folder]), $user);
|
||||
}
|
||||
|
||||
return new FolderResource($folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, Folder> $folders
|
||||
*/
|
||||
private function attachTrails(Collection $folders, User $user): void
|
||||
{
|
||||
$trails = $this->trails->ancestors($folders, $user);
|
||||
|
||||
foreach ($folders as $folder) {
|
||||
$folder->setRelation('trail', collect($trails[$folder->id] ?? []));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The parent must be a folder this caller's library shows them — the
|
||||
* same lookup the web screen makes, answering 404 otherwise.
|
||||
*/
|
||||
private function resolveParent(User $user, ?int $parentId): ?Folder
|
||||
{
|
||||
if ($parentId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var Builder<Folder> $folders */
|
||||
$folders = $this->scope->folders($user);
|
||||
|
||||
return $folders->findOrFail($parentId);
|
||||
}
|
||||
}
|
||||
@@ -129,9 +129,11 @@ class FileResource extends JsonResource
|
||||
'name' => $this->nextVersion->name,
|
||||
]),
|
||||
|
||||
// GET /folders/{id} has the rest, its place in the tree included.
|
||||
'folder' => $this->whenLoaded('folder', fn (): ?array => $this->folder === null ? null : [
|
||||
'id' => $this->folder->id,
|
||||
'name' => $this->folder->name,
|
||||
'parent_id' => $this->folder->parent_id,
|
||||
]),
|
||||
|
||||
// Name only. The uploader is a user record; their email address
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Files\Http\Resources\Api;
|
||||
|
||||
use App\Modules\Files\Access\ClientIdentityScope;
|
||||
use App\Modules\Files\Models\Folder;
|
||||
use App\Modules\Files\Models\FolderAssignment;
|
||||
use App\Modules\Groups\Models\Group;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin Folder
|
||||
*
|
||||
* Every field is listed explicitly, never $folder->toArray(), for the same
|
||||
* reason as FileResource: the next migration must not publish itself.
|
||||
*
|
||||
* `ancestors` and `path` come from FolderTrails, loaded by the controller
|
||||
* for a whole page at once, and are trimmed to the folders the caller may
|
||||
* see. The assignment list is narrowed per entry by ClientIdentityScope,
|
||||
* exactly as FileResource narrows a file's.
|
||||
*/
|
||||
class FolderResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$viewer = $request->user();
|
||||
$identity = app(ClientIdentityScope::class);
|
||||
$groupMorph = (new Group)->getMorphClass();
|
||||
|
||||
/** @var list<array{id: int, name: string}> $ancestors */
|
||||
$ancestors = $this->relationLoaded('trail') ? $this->getRelation('trail')->all() : [];
|
||||
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'parent_id' => $this->parent_id,
|
||||
// The folders above this one, root first, as far up as the
|
||||
// caller may see. Empty for a folder at the top of the library.
|
||||
'ancestors' => $ancestors,
|
||||
// The same trail as one string, this folder included:
|
||||
// "Clients / Acme / 2026". For display; match on ids, since a
|
||||
// folder name may itself contain " / ".
|
||||
'path' => implode(' / ', [...array_column($ancestors, 'name'), $this->name]),
|
||||
// Read-only here. Making a folder public publishes everything
|
||||
// inside it, and is done on the web.
|
||||
'public' => (bool) $this->public,
|
||||
'created_at' => $this->created_at?->toIso8601String(),
|
||||
'updated_at' => $this->updated_at?->toIso8601String(),
|
||||
'assignments' => $this->whenLoaded('assignments', fn (): array => $this->assignments
|
||||
->filter(fn (FolderAssignment $assignment): bool => $assignment->assignable_type === $groupMorph
|
||||
? $identity->permitsGroupId($viewer, (int) $assignment->assignable_id)
|
||||
: $identity->permitsClientId($viewer, (int) $assignment->assignable_id))
|
||||
->map(fn (FolderAssignment $assignment): array => [
|
||||
'type' => $assignment->assignable_type === $groupMorph ? 'group' : 'client',
|
||||
'id' => $assignment->assignable_id,
|
||||
'name' => $assignment->assignable?->getAttribute('name'),
|
||||
])
|
||||
->values()
|
||||
->all()),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* GET /api/v1/folders walks folders ordered by (updated_at, id), like every
|
||||
* list endpoint — see App\Modules\Api\Support\PollingQuery. Same reasoning
|
||||
* as the files index: without it, every poll is a filesort over the table.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('folders', function (Blueprint $table) {
|
||||
$table->index(['updated_at', 'id'], 'folders_updated_at_id_index');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('folders', function (Blueprint $table) {
|
||||
$table->dropIndex('folders_updated_at_id_index');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -13,6 +13,8 @@ use App\Modules\Comments\Http\Controllers\Api\CommentModerationController;
|
||||
use App\Modules\Comments\Http\Controllers\Api\FileCommentsController;
|
||||
use App\Modules\Files\Http\Controllers\Api\FileAssignmentsController;
|
||||
use App\Modules\Files\Http\Controllers\Api\FilesController;
|
||||
use App\Modules\Files\Http\Controllers\Api\FolderAssignmentsController as ApiFolderAssignmentsController;
|
||||
use App\Modules\Files\Http\Controllers\Api\FoldersController;
|
||||
use App\Modules\Files\Http\Controllers\Api\FileVersionsController as ApiFileVersionsController;
|
||||
use App\Modules\Files\Http\Controllers\ChunkedUploadsController;
|
||||
use App\Modules\Files\Http\Controllers\FileDownloadController;
|
||||
@@ -160,6 +162,40 @@ Route::middleware(['auth:sanctum', 'api-active', 'staff-token'])->group(function
|
||||
->name('api.files.version.destroy');
|
||||
});
|
||||
|
||||
/*
|
||||
|----------------------------------------------------------------------
|
||||
| Folders
|
||||
|----------------------------------------------------------------------
|
||||
|
|
||||
| Reading is FolderPolicy::view()'s staff branch, the same three keys
|
||||
| as reading files. Creating is `create_own_folders`, as on the web
|
||||
| (the controller asks for `upload` with it, as the web does). Renaming,
|
||||
| moving and sharing are "may edit", deleting is "may delete": both
|
||||
| keys of each pair appear, and FolderPolicy decides which one applies
|
||||
| to a given folder.
|
||||
|
|
||||
*/
|
||||
Route::middleware('token-can:upload,edit_files,edit_others_files')->group(function () {
|
||||
Route::get('folders', [FoldersController::class, 'index'])->name('api.folders.index');
|
||||
Route::get('folders/{folder}', [FoldersController::class, 'show'])->name('api.folders.show');
|
||||
});
|
||||
|
||||
Route::post('folders', [FoldersController::class, 'store'])
|
||||
->middleware('token-can:create_own_folders')
|
||||
->name('api.folders.store');
|
||||
|
||||
Route::middleware('token-can:edit_files,edit_others_files')->group(function () {
|
||||
Route::patch('folders/{folder}', [FoldersController::class, 'update'])->name('api.folders.update');
|
||||
Route::post('folders/{folder}/assignments', [ApiFolderAssignmentsController::class, 'store'])
|
||||
->name('api.folders.assignments.store');
|
||||
Route::delete('folders/{folder}/assignments', [ApiFolderAssignmentsController::class, 'destroy'])
|
||||
->name('api.folders.assignments.destroy');
|
||||
});
|
||||
|
||||
Route::delete('folders/{folder}', [FoldersController::class, 'destroy'])
|
||||
->middleware('token-can:delete_files,delete_others_files')
|
||||
->name('api.folders.destroy');
|
||||
|
||||
/*
|
||||
|----------------------------------------------------------------------
|
||||
| Comments
|
||||
|
||||
Reference in New Issue
Block a user