Changeset View
Standalone View
src/app/Http/Controllers/API/V4/FsController.php
- This file was moved from src/app/Http/Controllers/API/V4/FilesController.php.
<?php | <?php | ||||
namespace App\Http\Controllers\API\V4; | namespace App\Http\Controllers\API\V4; | ||||
use App\Backends\Storage; | use App\Backends\Storage; | ||||
use App\Fs\Item; | use App\Fs\Item; | ||||
use App\Fs\Property; | use App\Fs\Property; | ||||
use App\Http\Controllers\RelationController; | use App\Http\Controllers\RelationController; | ||||
use App\Rules\FileName; | use App\Rules\FileName; | ||||
use App\User; | use App\User; | ||||
use App\Utils; | use App\Utils; | ||||
use Illuminate\Http\Request; | use Illuminate\Http\Request; | ||||
use Illuminate\Support\Facades\Cache; | use Illuminate\Support\Facades\Cache; | ||||
use Illuminate\Support\Facades\Validator; | use Illuminate\Support\Facades\Validator; | ||||
class FilesController extends RelationController | class FsController extends RelationController | ||||
{ | { | ||||
protected const READ = 'r'; | protected const READ = 'r'; | ||||
protected const WRITE = 'w'; | protected const WRITE = 'w'; | ||||
protected const TYPE_COLLECTION = 'collection'; | |||||
protected const TYPE_FILE = 'file'; | |||||
protected const TYPE_UNKNOWN = 'unknown'; | |||||
/** @var string Resource localization label */ | /** @var string Resource localization label */ | ||||
protected $label = 'file'; | protected $label = 'file'; | ||||
/** @var string Resource model name */ | /** @var string Resource model name */ | ||||
protected $model = Item::class; | protected $model = Item::class; | ||||
/** | /** | ||||
* Delete a file. | * Delete a file. | ||||
* | * | ||||
* @param string $id File identifier | * @param string $id File identifier | ||||
* | * | ||||
* @return \Illuminate\Http\JsonResponse The response | * @return \Illuminate\Http\JsonResponse The response | ||||
*/ | */ | ||||
public function destroy($id) | public function destroy($id) | ||||
{ | { | ||||
// Only the file owner can do that, for now | // Only the file owner can do that, for now | ||||
$file = $this->inputFile($id, null); | $file = $this->inputItem($id, null); | ||||
if (is_int($file)) { | if (is_int($file)) { | ||||
return $this->errorResponse($file); | return $this->errorResponse($file); | ||||
} | } | ||||
// Here we're just marking the file as deleted, it will be removed from the | // Here we're just marking the file as deleted, it will be removed from the | ||||
// storage later with the fs:expunge command | // storage later with the fs:expunge command | ||||
$file->delete(); | $file->delete(); | ||||
if ($file->type & Item::TYPE_COLLECTION) { | |||||
return response()->json([ | |||||
'status' => 'success', | |||||
'message' => self::trans('app.collection-delete-success'), | |||||
]); | |||||
} | |||||
return response()->json([ | return response()->json([ | ||||
'status' => 'success', | 'status' => 'success', | ||||
'message' => self::trans('app.file-delete-success'), | 'message' => self::trans('app.file-delete-success'), | ||||
]); | ]); | ||||
} | } | ||||
/** | /** | ||||
* Fetch content of a file. | * Fetch content of a file. | ||||
Show All 24 Lines | class FsController extends RelationController | ||||
* | * | ||||
* @param string $fileId The file identifier. | * @param string $fileId The file identifier. | ||||
* | * | ||||
* @return \Illuminate\Http\JsonResponse | * @return \Illuminate\Http\JsonResponse | ||||
*/ | */ | ||||
public function getPermissions($fileId) | public function getPermissions($fileId) | ||||
{ | { | ||||
// Only the file owner can do that, for now | // Only the file owner can do that, for now | ||||
$file = $this->inputFile($fileId, null); | $file = $this->inputItem($fileId, null); | ||||
if (is_int($file)) { | if (is_int($file)) { | ||||
return $this->errorResponse($file); | return $this->errorResponse($file); | ||||
} | } | ||||
$result = $file->properties()->where('key', 'like', 'share-%')->get()->map( | $result = $file->properties()->where('key', 'like', 'share-%')->get()->map( | ||||
fn($prop) => self::permissionToClient($prop->key, $prop->value) | fn($prop) => self::permissionToClient($prop->key, $prop->value) | ||||
); | ); | ||||
Show All 11 Lines | class FsController extends RelationController | ||||
* | * | ||||
* @param string $fileId The file identifier. | * @param string $fileId The file identifier. | ||||
* | * | ||||
* @return \Illuminate\Http\JsonResponse | * @return \Illuminate\Http\JsonResponse | ||||
*/ | */ | ||||
public function createPermission($fileId) | public function createPermission($fileId) | ||||
{ | { | ||||
// Only the file owner can do that, for now | // Only the file owner can do that, for now | ||||
$file = $this->inputFile($fileId, null); | $file = $this->inputItem($fileId, null); | ||||
if (is_int($file)) { | if (is_int($file)) { | ||||
return $this->errorResponse($file); | return $this->errorResponse($file); | ||||
} | } | ||||
// Validate/format input | // Validate/format input | ||||
$v = Validator::make(request()->all(), [ | $v = Validator::make(request()->all(), [ | ||||
'user' => 'email|required', | 'user' => 'email|required', | ||||
▲ Show 20 Lines • Show All 44 Lines • ▼ Show 20 Lines | class FsController extends RelationController | ||||
* @param string $fileId The file identifier. | * @param string $fileId The file identifier. | ||||
* @param string $id The file permission identifier. | * @param string $id The file permission identifier. | ||||
* | * | ||||
* @return \Illuminate\Http\JsonResponse | * @return \Illuminate\Http\JsonResponse | ||||
*/ | */ | ||||
public function deletePermission($fileId, $id) | public function deletePermission($fileId, $id) | ||||
{ | { | ||||
// Only the file owner can do that, for now | // Only the file owner can do that, for now | ||||
$file = $this->inputFile($fileId, null); | $file = $this->inputItem($fileId, null); | ||||
if (is_int($file)) { | if (is_int($file)) { | ||||
return $this->errorResponse($file); | return $this->errorResponse($file); | ||||
} | } | ||||
$property = $file->properties()->where('key', $id)->first(); | $property = $file->properties()->where('key', $id)->first(); | ||||
if (!$property) { | if (!$property) { | ||||
Show All 15 Lines | class FsController extends RelationController | ||||
* @param string $fileId The file identifier. | * @param string $fileId The file identifier. | ||||
* @param string $id The file permission identifier. | * @param string $id The file permission identifier. | ||||
* | * | ||||
* @return \Illuminate\Http\JsonResponse | * @return \Illuminate\Http\JsonResponse | ||||
*/ | */ | ||||
public function updatePermission(Request $request, $fileId, $id) | public function updatePermission(Request $request, $fileId, $id) | ||||
{ | { | ||||
// Only the file owner can do that, for now | // Only the file owner can do that, for now | ||||
$file = $this->inputFile($fileId, null); | $file = $this->inputItem($fileId, null); | ||||
if (is_int($file)) { | if (is_int($file)) { | ||||
return $this->errorResponse($file); | return $this->errorResponse($file); | ||||
} | } | ||||
$property = $file->properties()->where('key', $id)->first(); | $property = $file->properties()->where('key', $id)->first(); | ||||
if (!$property) { | if (!$property) { | ||||
▲ Show 20 Lines • Show All 41 Lines • ▼ Show 20 Lines | class FsController extends RelationController | ||||
* Listing of files (and folders). | * Listing of files (and folders). | ||||
* | * | ||||
* @return \Illuminate\Http\JsonResponse | * @return \Illuminate\Http\JsonResponse | ||||
*/ | */ | ||||
public function index() | public function index() | ||||
{ | { | ||||
$search = trim(request()->input('search')); | $search = trim(request()->input('search')); | ||||
$page = intval(request()->input('page')) ?: 1; | $page = intval(request()->input('page')) ?: 1; | ||||
$parent = request()->input('parent'); | |||||
$type = request()->input('type'); | |||||
$pageSize = 100; | $pageSize = 100; | ||||
$hasMore = false; | $hasMore = false; | ||||
$user = $this->guard()->user(); | $user = $this->guard()->user(); | ||||
$result = $user->fsItems()->select('fs_items.*', 'fs_properties.value as name') | $result = $user->fsItems()->select('fs_items.*', 'fs_properties.value as name'); | ||||
->join('fs_properties', 'fs_items.id', '=', 'fs_properties.item_id') | |||||
if ($parent) { | |||||
$result->join('fs_relations', 'fs_items.id', '=', 'fs_relations.related_id') | |||||
->where('fs_relations.item_id', $parent); | |||||
} else { | |||||
$result->leftJoin('fs_relations', 'fs_items.id', '=', 'fs_relations.related_id') | |||||
->whereNull('fs_relations.related_id'); | |||||
} | |||||
// Add properties | |||||
$result->join('fs_properties', 'fs_items.id', '=', 'fs_properties.item_id') | |||||
->whereNot('type', '&', Item::TYPE_INCOMPLETE) | ->whereNot('type', '&', Item::TYPE_INCOMPLETE) | ||||
->where('key', 'name'); | ->where('key', 'name'); | ||||
if ($type) { | |||||
if ($type == self::TYPE_COLLECTION) { | |||||
$result->where('type', '&', Item::TYPE_COLLECTION); | |||||
} else { | |||||
$result->where('type', '&', Item::TYPE_FILE); | |||||
} | |||||
} | |||||
if (strlen($search)) { | if (strlen($search)) { | ||||
$result->whereLike('fs_properties.value', $search); | $result->whereLike('fs_properties.value', $search); | ||||
} | } | ||||
$result = $result->orderBy('name') | $result = $result->orderBy('name') | ||||
->limit($pageSize + 1) | ->limit($pageSize + 1) | ||||
->offset($pageSize * ($page - 1)) | ->offset($pageSize * ($page - 1)) | ||||
->get(); | ->get(); | ||||
Show All 26 Lines | class FsController extends RelationController | ||||
* Fetch the specific file metadata or content. | * Fetch the specific file metadata or content. | ||||
* | * | ||||
* @param string $id The file identifier. | * @param string $id The file identifier. | ||||
* | * | ||||
* @return \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\StreamedResponse | * @return \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\StreamedResponse | ||||
*/ | */ | ||||
public function show($id) | public function show($id) | ||||
{ | { | ||||
$file = $this->inputFile($id, self::READ); | $file = $this->inputItem($id, self::READ); | ||||
if (is_int($file)) { | if (is_int($file)) { | ||||
return $this->errorResponse($file); | return $this->errorResponse($file); | ||||
} | } | ||||
$response = $this->objectToClient($file, true); | $response = $this->objectToClient($file, true); | ||||
if (request()->input('downloadUrl')) { | if (request()->input('downloadUrl')) { | ||||
// Generate a download URL (that does not require authentication) | // Generate a download URL (that does not require authentication) | ||||
$downloadId = Utils::uuidStr(); | $downloadId = Utils::uuidStr(); | ||||
Cache::add('download:' . $downloadId, $file->id, 60); | Cache::add('download:' . $downloadId, $file->id, 60); | ||||
$response['downloadUrl'] = Utils::serviceUrl('api/v4/files/downloads/' . $downloadId); | $response['downloadUrl'] = Utils::serviceUrl('api/v4/fs/downloads/' . $downloadId); | ||||
} elseif (request()->input('download')) { | } elseif (request()->input('download')) { | ||||
// Return the file content | // Return the file content | ||||
return Storage::fileDownload($file); | return Storage::fileDownload($file); | ||||
} | } | ||||
$response['mtime'] = $file->updated_at->format('Y-m-d H:i'); | $response['mtime'] = $file->updated_at->format('Y-m-d H:i'); | ||||
// TODO: Handle read-write/full access rights | // TODO: Handle read-write/full access rights | ||||
$isOwner = $this->guard()->user()->id == $file->user_id; | $isOwner = $this->guard()->user()->id == $file->user_id; | ||||
$response['canUpdate'] = $isOwner; | $response['canUpdate'] = $isOwner; | ||||
$response['canDelete'] = $isOwner; | $response['canDelete'] = $isOwner; | ||||
$response['isOwner'] = $isOwner; | $response['isOwner'] = $isOwner; | ||||
return response()->json($response); | return response()->json($response); | ||||
} | } | ||||
private function deduplicateOrCreate(Request $request, $type) | |||||
{ | |||||
$user = $this->guard()->user(); | |||||
$item = null; | |||||
if ($request->has('deduplicate-property')) { | |||||
//query for item by deduplicate-value | |||||
$result = $user->fsItems()->select('fs_items.*'); | |||||
$result->join('fs_properties', function ($join) use ($request) { | |||||
$join->on('fs_items.id', '=', 'fs_properties.item_id') | |||||
->where('fs_properties.key', $request->input('deduplicate-property')); | |||||
}) | |||||
->where('type', '&', $type); | |||||
$result->whereLike('fs_properties.value', $request->input('deduplicate-value')); | |||||
$item = $result->first(); | |||||
} | |||||
if (!$item) { | |||||
$item = $user->fsItems()->create(['type' => $type]); | |||||
} | |||||
return $item; | |||||
} | |||||
/** | |||||
* Create a new collection. | |||||
* | |||||
* @param \Illuminate\Http\Request $request The API request. | |||||
* | |||||
* @return \Illuminate\Http\JsonResponse The response | |||||
*/ | |||||
private function createCollection(Request $request) | |||||
{ | |||||
// Validate file name input | |||||
$v = Validator::make($request->all(), [ | |||||
machniak: I think we should have some sanity checks on the input. | |||||
'name' => ['required', new FileName()], | |||||
'deviceId' => ['max:255'], | |||||
'collectionType' => ['max:255'], | |||||
]); | |||||
if ($v->fails()) { | |||||
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); | |||||
Not Done Inline ActionsSince the input is JSON, we can just have a "properties" (or "settings") property. machniak: Since the input is JSON, we can just have a "properties" (or "settings") property. | |||||
Done Inline ActionsI'm using the prefix because the mobile app uses query parameters to set the properties. We can't really use the json body as is, because the body is the file. mollekopf: I'm using the prefix because the mobile app uses query parameters to set the properties. We… | |||||
Not Done Inline ActionsWhen creating a collection we could use JSON input, but ok. For consistency we should probably use headers, e.g. X-Kolab-Property-* machniak: When creating a collection we could use JSON input, but ok. For consistency we should probably… | |||||
Done Inline ActionsRight, I am actually using a json body for collections. In the mobile app I'm using json for collections and parameters for files, which I agree is not fantastic, but works out fine on the controller side due to laravels abstraction. I suppose we could use headers, I didn't because the API felt a bit clunkier, and we've been using parameters for e.g. the name.
mollekopf: Right, I am actually using a json body for collections.
Laravel's input() picks data up from… | |||||
} | |||||
Done Inline ActionsKey length is limited to 191 chars (or actually bytes). It should be validated too, e.g. accept only letters, digits and '-' or dots. machniak: Key length is limited to 191 chars (or actually bytes). It should be validated too, e.g. accept… | |||||
$properties = [ | |||||
'name' => $request->input('name'), | |||||
'deviceId' => $request->input('deviceId'), | |||||
'collectionType' => $request->input('collectionType'), | |||||
]; | |||||
foreach ($request->all() as $key => $value) { | |||||
if (str_starts_with($key, "property-")) { | |||||
$propertyKey = substr($key, 9); | |||||
if (strlen($propertyKey) > 191) { | |||||
return response()->json([ | |||||
'status' => 'error', | |||||
Done Inline ActionsYou have to pass the 'attribute' value to trans() here. machniak: You have to pass the 'attribute' value to trans() here. | |||||
'errors' => [self::trans('validation.max.string', ['attribute' => $propertyKey, 'max' => 191])] | |||||
], 422); | |||||
} | |||||
Done Inline ActionsIt would be good to use trans() here. machniak: It would be good to use trans() here. | |||||
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $propertyKey)) { | |||||
return response()->json([ | |||||
Done Inline ActionsI'd suggest to create $properties array and pass it to setProperties() once at the end. Also, I'd move this whole foreach before deduplicateOrCreate() call, as it's merely a validation logic. As it is right now it can create the item and then return 422 on propertyKey validation error. machniak: I'd suggest to create $properties array and pass it to setProperties() once at the end. Also… | |||||
'status' => 'error', | |||||
'errors' => [self::trans('validation.regex_format', [ | |||||
'attribute' => $propertyKey, | |||||
'format' => "a-zA-Z0-9_-" | |||||
])] | |||||
], 422); | |||||
} | |||||
$properties[$propertyKey] = $value; | |||||
} | |||||
} | |||||
$item = $this->deduplicateOrCreate($request, Item::TYPE_COLLECTION); | |||||
$item->setProperties($properties); | |||||
if ($parent = $request->input('parent')) { | |||||
$item->parents()->sync([$parent]); | |||||
} | |||||
$response = []; | |||||
$response['status'] = 'success'; | |||||
$response['id'] = $item->id; | |||||
$response['message'] = self::trans('app.collection-create-success'); | |||||
return response()->json($response); | |||||
} | |||||
/** | /** | ||||
* Create a new file. | * Create a new file. | ||||
* | * | ||||
* @param \Illuminate\Http\Request $request The API request. | * @param \Illuminate\Http\Request $request The API request. | ||||
* | * | ||||
* @return \Illuminate\Http\JsonResponse The response | * @return \Illuminate\Http\JsonResponse The response | ||||
*/ | */ | ||||
public function store(Request $request) | public function store(Request $request) | ||||
{ | { | ||||
$user = $this->guard()->user(); | $type = $request->input('type'); | ||||
if ($type == self::TYPE_COLLECTION) { | |||||
Done Inline Actionsself::TYPE_COLLECTION machniak: `self::TYPE_COLLECTION` | |||||
return $this->createCollection($request); | |||||
} | |||||
// Validate file name input | // Validate file name input | ||||
$v = Validator::make($request->all(), ['name' => ['required', new FileName($user)]]); | $v = Validator::make($request->all(), ['name' => ['required', new FileName()]]); | ||||
if ($v->fails()) { | if ($v->fails()) { | ||||
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); | return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); | ||||
} | } | ||||
$filename = $request->input('name'); | $filename = $request->input('name'); | ||||
$media = $request->input('media'); | $media = $request->input('media'); | ||||
// FIXME: Normally people just drag and drop/upload files. | |||||
// The client side will not know whether the file with the same name | |||||
// already exists or not. So, in such a case should we throw | |||||
// an error or accept the request as an update? | |||||
$params = []; | $params = []; | ||||
$params['mimetype'] = $request->headers->get('Content-Type', null); | |||||
if ($media == 'resumable') { | if ($media == 'resumable') { | ||||
$params['uploadId'] = 'resumable'; | $params['uploadId'] = 'resumable'; | ||||
$params['size'] = $request->input('size'); | $params['size'] = $request->input('size'); | ||||
$params['from'] = $request->input('from') ?: 0; | $params['from'] = $request->input('from') ?: 0; | ||||
} | } | ||||
// TODO: Delete the existing incomplete file with the same name? | // TODO: Delete the existing incomplete file with the same name? | ||||
$properties = ['name' => $filename]; | |||||
$file = $user->fsItems()->create(['type' => Item::TYPE_INCOMPLETE | Item::TYPE_FILE]); | foreach ($request->all() as $key => $value) { | ||||
Done Inline ActionsYou dropped TYPE_INCOMPLETE here. machniak: You dropped TYPE_INCOMPLETE here. | |||||
$file->setProperty('name', $filename); | if (str_starts_with($key, "property-")) { | ||||
$propertyKey = substr($key, 9); | |||||
if (strlen($propertyKey) > 191) { | |||||
return response()->json([ | |||||
'status' => 'error', | |||||
'errors' => [self::trans('validation.max.string', ['attribute' => $propertyKey, 'max' => 191])] | |||||
], 422); | |||||
} | |||||
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $propertyKey)) { | |||||
return response()->json([ | |||||
'status' => 'error', | |||||
'errors' => [self::trans('validation.regex_format', [ | |||||
'attribute' => $propertyKey, | |||||
'format' => "a-zA-Z0-9_-" | |||||
])] | |||||
], 422); | |||||
} | |||||
$properties[$propertyKey] = $value; | |||||
} | |||||
} | |||||
$file = $this->deduplicateOrCreate($request, Item::TYPE_INCOMPLETE | Item::TYPE_FILE); | |||||
$file->setProperties($properties); | |||||
if ($parentHeader = $request->headers->get('X-Kolab-Parents', null)) { | |||||
$parents = explode(',', $parentHeader); | |||||
$file->parents()->sync($parents); | |||||
} | |||||
if ($parent = $request->input('parent')) { | |||||
$file->parents()->sync([$parent]); | |||||
} | |||||
try { | try { | ||||
$response = Storage::fileInput($request->getContent(true), $params, $file); | $response = Storage::fileInput($request->getContent(true), $params, $file); | ||||
Not Done Inline ActionsWe need the same properties validation logic as in createCollection(). I also think that maybe we should for now limit the size of their values. machniak: We need the same properties validation logic as in createCollection(). I also think that maybe… | |||||
Done Inline ActionsWhy? We always have the database imposed limits. mollekopf: Why? We always have the database imposed limits. | |||||
Not Done Inline ActionsAh, I forgot that TEXT type is limited to 65,535 bytes. So, I think it's not that big of a deal. machniak: Ah, I forgot that TEXT type is limited to 65,535 bytes. So, I think it's not that big of a… | |||||
$response['status'] = 'success'; | $response['status'] = 'success'; | ||||
if (!empty($response['id'])) { | if (!empty($response['id'])) { | ||||
$response += $this->objectToClient($file, true); | $response += $this->objectToClient($file, true); | ||||
$response['message'] = self::trans('app.file-create-success'); | $response['message'] = self::trans('app.file-create-success'); | ||||
} | } | ||||
} catch (\Exception $e) { | } catch (\Exception $e) { | ||||
\Log::error($e); | \Log::error($e); | ||||
$file->delete(); | $file->delete(); | ||||
Not Done Inline ActionsThis is redundant if we have X-Kolab-Parents. Also, I'd move parents handling to be after the try/catch block below. machniak: This is redundant if we have X-Kolab-Parents. Also, I'd move parents handling to be after the… | |||||
Done Inline ActionsWe're using this to set the parent via parameters. mollekopf: We're using this to set the parent via parameters. | |||||
return $this->errorResponse(500); | return $this->errorResponse(500); | ||||
} | } | ||||
return response()->json($response); | return response()->json($response); | ||||
} | } | ||||
/** | /** | ||||
* Update a file. | * Update a file. | ||||
* | * | ||||
* @param \Illuminate\Http\Request $request The API request. | * @param \Illuminate\Http\Request $request The API request. | ||||
* @param string $id File identifier | * @param string $id File identifier | ||||
* | * | ||||
* @return \Illuminate\Http\JsonResponse The response | * @return \Illuminate\Http\JsonResponse The response | ||||
*/ | */ | ||||
public function update(Request $request, $id) | public function update(Request $request, $id) | ||||
{ | { | ||||
$file = $this->inputFile($id, self::WRITE); | $file = $this->inputItem($id, self::WRITE); | ||||
if (is_int($file)) { | if (is_int($file)) { | ||||
return $this->errorResponse($file); | return $this->errorResponse($file); | ||||
} | } | ||||
$media = $request->input('media') ?: 'metadata'; | $media = $request->input('media') ?: 'metadata'; | ||||
Done Inline ActionsI'd move parents handling after any input validation. machniak: I'd move parents handling after any input validation. | |||||
if ($media == 'metadata') { | if ($media == 'metadata') { | ||||
$filename = $request->input('name'); | $filename = $request->input('name'); | ||||
// Validate file name input | // Validate file name input | ||||
if ($filename != $file->getProperty('name')) { | if ($filename != $file->getProperty('name')) { | ||||
$v = Validator::make($request->all(), ['name' => [new FileName($file->user)]]); | $v = Validator::make($request->all(), ['name' => [new FileName()]]); | ||||
if ($v->fails()) { | if ($v->fails()) { | ||||
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); | return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); | ||||
} | } | ||||
$file->setProperty('name', $filename); | $file->setProperty('name', $filename); | ||||
} | } | ||||
// $file->save(); | |||||
if ($parentHeader = $request->headers->get('X-Kolab-Parents', null)) { | |||||
$parents = explode(',', $parentHeader); | |||||
$file->parents()->sync($parents); | |||||
} | |||||
if ($parentHeader = $request->headers->get('X-Kolab-Add-Parents', null)) { | |||||
$parents = explode(',', $parentHeader); | |||||
$file->parents()->syncWithoutDetaching($parents); | |||||
} | |||||
if ($parentHeader = $request->headers->get('X-Kolab-Remove-Parents', null)) { | |||||
$parents = explode(',', $parentHeader); | |||||
$file->parents()->detach($parents); | |||||
} | |||||
$file->save(); | |||||
} elseif ($media == 'resumable' || $media == 'content') { | } elseif ($media == 'resumable' || $media == 'content') { | ||||
$params = []; | $params = []; | ||||
if ($media == 'resumable') { | if ($media == 'resumable') { | ||||
$params['uploadId'] = 'resumable'; | $params['uploadId'] = 'resumable'; | ||||
$params['size'] = $request->input('size'); | $params['size'] = $request->input('size'); | ||||
$params['from'] = $request->input('from') ?: 0; | $params['from'] = $request->input('from') ?: 0; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 97 Lines • ▼ Show 20 Lines | class FsController extends RelationController | ||||
/** | /** | ||||
* Get the input file object, check permissions | * Get the input file object, check permissions | ||||
* | * | ||||
* @param string $fileId File or file permission identifier | * @param string $fileId File or file permission identifier | ||||
* @param ?string $permission Required access rights | * @param ?string $permission Required access rights | ||||
* | * | ||||
* @return \App\Fs\Item|int File object or error code | * @return \App\Fs\Item|int File object or error code | ||||
*/ | */ | ||||
protected function inputFile($fileId, $permission) | protected function inputItem($fileId, $permission) | ||||
{ | { | ||||
$user = $this->guard()->user(); | $user = $this->guard()->user(); | ||||
$isShare = str_starts_with($fileId, 'share-'); | $isShare = str_starts_with($fileId, 'share-'); | ||||
// Access via file permission identifier | // Access via file permission identifier | ||||
if ($isShare) { | if ($isShare) { | ||||
$property = Property::where('key', $fileId)->first(); | $property = Property::where('key', $fileId)->first(); | ||||
if (!$property) { | if (!$property) { | ||||
return 404; | return 404; | ||||
} | } | ||||
list($acl_user, $acl) = explode(':', $property->value); | list($acl_user, $acl) = explode(':', $property->value); | ||||
if (!$permission || $acl_user != $user->email || strpos($acl, $permission) === false) { | if (!$permission || $acl_user != $user->email || strpos($acl, $permission) === false) { | ||||
return 403; | return 403; | ||||
} | } | ||||
$fileId = $property->item_id; | $fileId = $property->item_id; | ||||
} | } | ||||
$file = Item::find($fileId); | $file = Item::find($fileId); | ||||
if (!$file || !($file->type & Item::TYPE_FILE) || ($file->type & Item::TYPE_INCOMPLETE)) { | if (!$file) { | ||||
return 404; | return 404; | ||||
} | } | ||||
if (!$isShare && $user->id != $file->user_id) { | if (!$isShare && $user->id != $file->user_id) { | ||||
return 403; | return 403; | ||||
} | } | ||||
if ($file->type & Item::TYPE_FILE && $file->type & Item::TYPE_INCOMPLETE) { | |||||
return 404; | |||||
} | |||||
return $file; | return $file; | ||||
} | } | ||||
/** | /** | ||||
* Prepare a file object for the UI. | * Prepare a file object for the UI. | ||||
* | * | ||||
* @param object $object An object | * @param object $object An object | ||||
* @param bool $full Include all object properties | * @param bool $full Include all object properties | ||||
* | * | ||||
* @return array Object information | * @return array Object information | ||||
*/ | */ | ||||
protected function objectToClient($object, bool $full = false): array | protected function objectToClient($object, bool $full = false): array | ||||
{ | { | ||||
$result = ['id' => $object->id]; | $result = ['id' => $object->id]; | ||||
if ($object->type & Item::TYPE_COLLECTION) { | |||||
Done Inline ActionsItem:: machniak: Item:: | |||||
$result['type'] = self::TYPE_COLLECTION; | |||||
} elseif ($object->type & Item::TYPE_FILE) { | |||||
$result['type'] = self::TYPE_FILE; | |||||
Done Inline ActionsI not "collection" but type & Item::TYPE_FILE then "file", else "incomplete" or "unknown" (or null). Also these should be constants probably machniak: I not "collection" but type & Item::TYPE_FILE then "file", else "incomplete" or "unknown" (or… | |||||
} else { | |||||
$result['type'] = self::TYPE_UNKNOWN; | |||||
} | |||||
if ($full) { | if ($full) { | ||||
$props = array_filter($object->getProperties(['name', 'size', 'mimetype'])); | $props = array_filter($object->getProperties(['name', 'size', 'mimetype'])); | ||||
// convert size to int and make sure the property exists | // convert size to int and make sure the property exists | ||||
$props['size'] = (int) ($props['size'] ?? 0); | $props['size'] = (int) ($props['size'] ?? 0); | ||||
$result += $props; | $result += $props; | ||||
} | } | ||||
return $result; | return $result; | ||||
} | } | ||||
} | } |
I think we should have some sanity checks on the input.