Changeset View
Changeset View
Standalone View
Standalone View
src/app/Http/Controllers/API/V4/FilesController.php
- This file was added.
<?php | |||||
namespace App\Http\Controllers\API\V4; | |||||
use App\Backends\Storage; | |||||
use App\Http\Controllers\RelationController; | |||||
use App\File; | |||||
use App\FilePermission; | |||||
use App\Library; | |||||
use App\User; | |||||
use Illuminate\Http\Request; | |||||
use Illuminate\Support\Facades\Validator; | |||||
class FilesController extends RelationController | |||||
{ | |||||
/** @var string Resource localization label */ | |||||
protected $label = 'file'; | |||||
/** @var string Resource model name */ | |||||
protected $model = File::class; | |||||
/** @var array Common object properties in the API response */ | |||||
protected $objectProps = ['mimetype', 'name', 'size']; | |||||
/** | |||||
* Delete a file. | |||||
* | |||||
* @param string $id File identifier | |||||
* | |||||
* @return \Illuminate\Http\JsonResponse The response | |||||
*/ | |||||
public function destroy($id) | |||||
{ | |||||
$file = File::find($id); | |||||
if (!$file) { | |||||
return $this->errorResponse(404); | |||||
} | |||||
if (!self::hasPermission($file, $this->guard()->user(), FilePermission::DELETE)) { | |||||
return $this->errorResponse(403); | |||||
} | |||||
// FIXME: Here we're just deleting the file, but maybe it would be better/faster | |||||
// to mark the file (record in db) as deleted and invoke a job to | |||||
// delete it asynchronously? | |||||
Storage::fileDelete($file); | |||||
$file->delete(); | |||||
return response()->json([ | |||||
'status' => 'success', | |||||
'message' => \trans('app.file-delete-success'), | |||||
]); | |||||
} | |||||
/** | |||||
* Fetch the permissions for the specific file. | |||||
* | |||||
* @param string $id The file identifier. | |||||
* | |||||
* @return \Illuminate\Http\JsonResponse | |||||
*/ | |||||
public function getPermissions($id) | |||||
{ | |||||
$file = File::find($id); | |||||
if (!$file) { | |||||
return $this->errorResponse(404); | |||||
} | |||||
// Only the folder owner can do that, for now | |||||
if ($this->guard()->user()->id != $file->library->user_id) { | |||||
return $this->errorResponse(403); | |||||
} | |||||
$result = $file->permissions()->orderBy('user')->get()->map( | |||||
function ($permission) { | |||||
// FIXME: Here we map internal format to the one supported | |||||
// by the ACL widget, but I guess we can get rid of this limitation | |||||
// in the future, if needed. | |||||
if ($permission->permissions & FilePermission::DELETE) { | |||||
$perms = 'full'; | |||||
} elseif ($permission->permissions & FilePermission::WRITE) { | |||||
$perms = 'read-write'; | |||||
} | |||||
return [ | |||||
'user' => $permission->user, | |||||
'permissions' => $perms ?? 'read-only', | |||||
]; | |||||
} | |||||
); | |||||
$result = [ | |||||
'list' => $result, | |||||
'count' => count($result), | |||||
]; | |||||
return response()->json($result); | |||||
} | |||||
/** | |||||
* Listing of files (and folders). | |||||
* | |||||
* @return \Illuminate\Http\JsonResponse | |||||
*/ | |||||
public function index() | |||||
{ | |||||
$search = trim(request()->input('search')); | |||||
$page = intval(request()->input('page')) ?: 1; | |||||
$pageSize = 100; | |||||
$hasMore = false; | |||||
$user = $this->guard()->user(); | |||||
if (request()->input('shared')) { | |||||
$result = File::join('file_permissions', 'file_permissions.file_id', '=', 'files.id') | |||||
->where('user', $user->email); | |||||
} else { | |||||
$library = $this->defaultLibrary($user); | |||||
$result = $library->files(); | |||||
} | |||||
$result = $result->where('mimetype', '<>', File::TYPE_INCOMPLETE) | |||||
->orderBy('name') | |||||
->limit($pageSize + 1) | |||||
->offset($pageSize * ($page - 1)) | |||||
->get(); | |||||
if (count($result) > $pageSize) { | |||||
$result->pop(); | |||||
$hasMore = true; | |||||
} | |||||
// Process the result | |||||
$result = $result->map( | |||||
function ($file) { | |||||
return $this->objectToClient($file); | |||||
} | |||||
); | |||||
$result = [ | |||||
'list' => $result, | |||||
'count' => count($result), | |||||
'hasMore' => $hasMore, | |||||
]; | |||||
return response()->json($result); | |||||
} | |||||
/** | |||||
* Set permissions for the specific file. | |||||
* | |||||
* @param string $id The file identifier. | |||||
* | |||||
* @return \Illuminate\Http\JsonResponse | |||||
*/ | |||||
public function setPermissions($id) | |||||
{ | |||||
$file = File::find($id); | |||||
if (!$file) { | |||||
return $this->errorResponse(404); | |||||
} | |||||
// Only the library owner can do that, for now | |||||
if ($this->guard()->user()->id != $file->library->user_id) { | |||||
return $this->errorResponse(403); | |||||
} | |||||
// Validate/format input | |||||
$input = []; | |||||
$errors = []; | |||||
foreach ((array) request()->input('permissions') as $idx => $entry) { | |||||
$acl = $entry['permissions'] ?? ''; | |||||
$user = $entry['user'] ?? ''; | |||||
if (empty($user)) { | |||||
continue; | |||||
} | |||||
// validate user email | |||||
$v = Validator::make(['email' => $user], ['email' => 'email']); | |||||
if ($v->fails()) { | |||||
$errors['user'][$idx] = \trans('validation.emailinvalid'); | |||||
} | |||||
// The ACL widget supports 'full', 'read-write', 'read-only', convert | |||||
// it to the internal format | |||||
if ($acl == 'full') { | |||||
$acl = FilePermission::DELETE | FilePermission::WRITE | FilePermission::READ; | |||||
} elseif ($acl == 'read-write') { | |||||
$acl = FilePermission::WRITE | FilePermission::READ; | |||||
} elseif ($acl == 'read-only') { | |||||
$acl = FilePermission::READ; | |||||
} else { | |||||
continue; | |||||
} | |||||
$input[$user] = $acl; | |||||
} | |||||
if (!empty($errors)) { | |||||
return response()->json(['status' => 'error', 'errors' => $errors], 422); | |||||
} | |||||
// Get existing permissions and compare with the new ones | |||||
// Update/delete existing entries | |||||
$file->permissions()->get()->each(function($permission) use (&$input) { | |||||
if (!empty($input[$permission->user])) { | |||||
$permission->permissions = $input[$permission->user]; | |||||
$permission->save(); | |||||
unset($input[$permission->user]); | |||||
} else { | |||||
$permission->delete(); | |||||
} | |||||
}); | |||||
// Create new permissions | |||||
foreach ($input as $user => $permissions) { | |||||
$file->permissions()->create([ | |||||
'user' => $user, | |||||
'permissions' => $permissions | |||||
]); | |||||
} | |||||
return response()->json([ | |||||
'status' => 'success', | |||||
'message' => \trans('app.file-permissions-update-success'), | |||||
]); | |||||
} | |||||
/** | |||||
* Fetch the specific file metadata or content. | |||||
* | |||||
* @param string $id The file identifier. | |||||
* | |||||
* @return \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\StreamedResponse | |||||
*/ | |||||
public function show($id) | |||||
{ | |||||
$file = File::find($id); | |||||
if (!$file) { | |||||
return $this->errorResponse(404); | |||||
} | |||||
if (!self::hasPermission($file, $this->guard()->user(), FilePermission::READ)) { | |||||
return $this->errorResponse(403); | |||||
} | |||||
if (request()->input('download')) { | |||||
return Storage::fileDownload($file); | |||||
} | |||||
$response = $file->toArray(); | |||||
return response()->json($response); | |||||
} | |||||
/** | |||||
* Create a new file. | |||||
* | |||||
* @param \Illuminate\Http\Request $request The API request. | |||||
* | |||||
* @return \Illuminate\Http\JsonResponse The response | |||||
*/ | |||||
public function store(Request $request) | |||||
{ | |||||
$user = $this->guard()->user(); | |||||
$library = $this->defaultLibrary($user); | |||||
$filename = $request->input('name'); | |||||
// TODO: Validate file name input | |||||
// TODO: Delete the existing incomplete file with the same name? | |||||
// 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 = ['name' => $filename]; | |||||
if ($upload = $request->input('upload')) { | |||||
$params['uploadId'] = $upload; | |||||
$params['size'] = $request->input('size'); | |||||
$params['from'] = $request->input('from'); | |||||
} | |||||
try { | |||||
$response = Storage::fileInput($library, $request->getContent(true), $params); | |||||
$response['status'] = 'success'; | |||||
if (!empty($response['id'])) { | |||||
$response['message'] = \trans('app.file-create-success'); | |||||
} | |||||
} catch (\Exception $e) { | |||||
\Log::error($e); | |||||
return $this->errorResponse(500); | |||||
} | |||||
return response()->json($response); | |||||
} | |||||
/** | |||||
* Update a file. | |||||
* | |||||
* @param \Illuminate\Http\Request $request The API request. | |||||
* @param string $id File identifier | |||||
* | |||||
* @return \Illuminate\Http\JsonResponse The response | |||||
*/ | |||||
public function update(Request $request, $id) | |||||
{ | |||||
$file = File::find($id); | |||||
if (empty($file)) { | |||||
return $this->errorResponse(404); | |||||
} | |||||
$user = $this->guard()->user(); | |||||
if (!self::hasPermission($file, $user, FilePermission::WRITE)) { | |||||
return $this->errorResponse(403); | |||||
} | |||||
// TODO | |||||
return response()->json([ | |||||
'status' => 'success', | |||||
'message' => \trans('app.file-update-success'), | |||||
]); | |||||
} | |||||
/** | |||||
* Get the user's default Library. Create one if it does not exist. | |||||
* | |||||
* @param \App\User $user User | |||||
* | |||||
* @return \App\Library The default library for the user | |||||
*/ | |||||
protected function defaultLibrary(User $user): Library | |||||
{ | |||||
$library = $user->libraries()->first(); | |||||
if (!$library) { | |||||
$library = $user->libraries()->create(['name' => null]); | |||||
} | |||||
return $library; | |||||
} | |||||
/** | |||||
* Checks whether the specified user has specified permissions to a file. | |||||
* | |||||
* @param \App\File $file The file | |||||
* @param \App\User $user Current user | |||||
* @param int $perms Permissions | |||||
* | |||||
* @return bool | |||||
*/ | |||||
protected static function hasPermission(File $file, User $user, int $perms) | |||||
{ | |||||
// File owner | |||||
if ($user->id == $file->library->user_id) { | |||||
return true; | |||||
} | |||||
return $file->hasPermission($user->email, $perms); | |||||
} | |||||
} |