Page MenuHomePhorge

D4190.1775452770.diff
No OneTemporary

Authored By
Unknown
Size
66 KB
Referenced Files
None
Subscribers
None

D4190.1775452770.diff

diff --git a/src/app/Backends/Storage.php b/src/app/Backends/Storage.php
--- a/src/app/Backends/Storage.php
+++ b/src/app/Backends/Storage.php
@@ -126,7 +126,8 @@
// Update the file type and size information
$file->setProperties([
'size' => $fileSize,
- 'mimetype' => self::mimetype($path),
+ // Pick the client-supplied mimetype if available, otherwise detect.
+ 'mimetype' => !empty($params['mimetype']) ? $params['mimetype'] : self::mimetype($path),
]);
// Assign the node to the file, "unlink" any old nodes of this file
diff --git a/src/app/Fs/Item.php b/src/app/Fs/Item.php
--- a/src/app/Fs/Item.php
+++ b/src/app/Fs/Item.php
@@ -24,7 +24,7 @@
use UuidStrKeyTrait;
public const TYPE_FILE = 1;
- public const TYPE_FOLDER = 2;
+ public const TYPE_COLLECTION = 2;
public const TYPE_INCOMPLETE = 4;
/** @var array<int, string> The attributes that are mass assignable */
@@ -167,4 +167,34 @@
);
}
}
+
+ /**
+ * Relations for this user
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ */
+ public function relations()
+ {
+ return $this->hasMany(Relation::class);
+ }
+
+ /**
+ * Relations for this item
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
+ */
+ public function children()
+ {
+ return $this->belongsToMany(Item::class, 'fs_relations', 'item', 'related');
+ }
+
+ /**
+ * Relations for this item
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
+ */
+ public function parents()
+ {
+ return $this->belongsToMany(Item::class, 'fs_relations', 'related', 'item');
+ }
}
diff --git a/src/app/Fs/Relation.php b/src/app/Fs/Relation.php
new file mode 100644
--- /dev/null
+++ b/src/app/Fs/Relation.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Fs;
+
+use Illuminate\Database\Eloquent\Casts\Attribute;
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * The eloquent definition of a filesystem item.
+ *
+ * @property string $id Item identifier
+ * @property string $related Item identifier
+ */
+class Relation extends Model
+{
+ /** @var array<int, string> The attributes that are mass assignable */
+ protected $fillable = ['item', 'related'];
+
+ /** @var string Database table name */
+ protected $table = 'fs_relations';
+}
diff --git a/src/app/Http/Controllers/API/V4/CompanionAppsController.php b/src/app/Http/Controllers/API/V4/CompanionAppsController.php
--- a/src/app/Http/Controllers/API/V4/CompanionAppsController.php
+++ b/src/app/Http/Controllers/API/V4/CompanionAppsController.php
@@ -247,7 +247,7 @@
'personal_access_client' => 0,
'password_client' => 1,
'revoked' => false,
- 'allowed_scopes' => "mfa"
+ 'allowed_scopes' => ["mfa", "files"]
]);
$client->save();
diff --git a/src/app/Http/Controllers/API/V4/FilesController.php b/src/app/Http/Controllers/API/V4/FsController.php
rename from src/app/Http/Controllers/API/V4/FilesController.php
rename to src/app/Http/Controllers/API/V4/FsController.php
--- a/src/app/Http/Controllers/API/V4/FilesController.php
+++ b/src/app/Http/Controllers/API/V4/FsController.php
@@ -13,7 +13,7 @@
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
-class FilesController extends RelationController
+class FsController extends RelationController
{
protected const READ = 'r';
protected const WRITE = 'w';
@@ -35,7 +35,7 @@
public function destroy($id)
{
// Only the file owner can do that, for now
- $file = $this->inputFile($id, null);
+ $file = $this->inputItem($id, null);
if (is_int($file)) {
return $this->errorResponse($file);
@@ -45,6 +45,13 @@
// storage later with the fs:expunge command
$file->delete();
+ if ($file->type & Item::TYPE_COLLECTION) {
+ return response()->json([
+ 'status' => 'success',
+ 'message' => \trans('app.collection-delete-success'),
+ ]);
+ }
+
return response()->json([
'status' => 'success',
'message' => \trans('app.file-delete-success'),
@@ -85,7 +92,7 @@
public function getPermissions($fileId)
{
// Only the file owner can do that, for now
- $file = $this->inputFile($fileId, null);
+ $file = $this->inputItem($fileId, null);
if (is_int($file)) {
return $this->errorResponse($file);
@@ -113,7 +120,7 @@
public function createPermission($fileId)
{
// Only the file owner can do that, for now
- $file = $this->inputFile($fileId, null);
+ $file = $this->inputItem($fileId, null);
if (is_int($file)) {
return $this->errorResponse($file);
@@ -174,7 +181,7 @@
public function deletePermission($fileId, $id)
{
// Only the file owner can do that, for now
- $file = $this->inputFile($fileId, null);
+ $file = $this->inputItem($fileId, null);
if (is_int($file)) {
return $this->errorResponse($file);
@@ -206,7 +213,7 @@
public function updatePermission(Request $request, $fileId, $id)
{
// Only the file owner can do that, for now
- $file = $this->inputFile($fileId, null);
+ $file = $this->inputItem($fileId, null);
if (is_int($file)) {
return $this->errorResponse($file);
@@ -264,15 +271,35 @@
{
$search = trim(request()->input('search'));
$page = intval(request()->input('page')) ?: 1;
+ $parent = request()->input('parent');
+ $type = request()->input('type');
$pageSize = 100;
$hasMore = false;
$user = $this->guard()->user();
- $result = $user->fsItems()->select('fs_items.*', 'fs_properties.value as name')
- ->join('fs_properties', 'fs_items.id', '=', 'fs_properties.item_id')
- ->whereNot('type', '&', Item::TYPE_INCOMPLETE)
- ->where('key', 'name');
+ $result = $user->fsItems()->select('fs_items.*', 'fs_properties.value as name');
+
+ if ($parent) {
+ $result->join('fs_relations', 'fs_items.id', '=', 'fs_relations.related')
+ ->where('fs_relations.item', $parent);
+ } else {
+ $result->leftJoin('fs_relations', 'fs_items.id', '=', 'fs_relations.related')
+ ->whereNull('fs_relations.related');
+ }
+
+ // Add properties
+ $result->join('fs_properties', 'fs_items.id', '=', 'fs_properties.item_id')
+ ->whereNot('type', '&', Item::TYPE_INCOMPLETE)
+ ->where('key', 'name');
+
+ if ($type) {
+ if ($type == "collection") {
+ $result->where('type', '&', Item::TYPE_COLLECTION);
+ } else {
+ $result->where('type', '&', Item::TYPE_FILE);
+ }
+ }
if (strlen($search)) {
$result->whereLike('fs_properties.value', $search);
@@ -316,7 +343,7 @@
*/
public function show($id)
{
- $file = $this->inputFile($id, self::READ);
+ $file = $this->inputItem($id, self::READ);
if (is_int($file)) {
return $this->errorResponse($file);
@@ -328,7 +355,7 @@
// Generate a download URL (that does not require authentication)
$downloadId = Utils::uuidStr();
Cache::add('download:' . $downloadId, $file->id, 60);
- $response['downloadUrl'] = Utils::serviceUrl('api/v4/files/downloads/' . $downloadId);
+ $response['downloadUrl'] = Utils::serviceUrl('api/v4/items/downloads/' . $downloadId);
} elseif (request()->input('download')) {
// Return the file content
return Storage::fileDownload($file);
@@ -345,6 +372,60 @@
return response()->json($response);
}
+
+ /**
+ * Create a new collection.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ private function createCollection(Request $request)
+ {
+ $user = $this->guard()->user();
+
+ $inputs = $request->all();
+ $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', '&', Item::TYPE_COLLECTION);
+
+ $result->whereLike('fs_properties.value', $request->input('deduplicate-value'));
+ $item = $result->first();
+ }
+
+ if (!$item) {
+ $item = $user->fsItems()->create(['type' => Item::TYPE_COLLECTION]);
+ }
+ $item->setProperties([
+ 'name' => $request->input('name'),
+ 'deviceId' => $request->input('deviceId'),
+ 'collectionType' => $request->input('collectionType'),
+ ]);
+
+ foreach ($inputs as $key => $value) {
+ if (str_starts_with($key, "property-")) {
+ $item->setProperty(substr($key, 9), $value);
+ }
+ }
+
+ if ($parent = $request->input('parent')) {
+ $item->parents()->attach([$parent]);
+ }
+
+ $response = [];
+ $response['status'] = 'success';
+ $response['id'] = $item->id;
+ $response['message'] = \trans('app.collection-create-success');
+
+ return response()->json($response);
+ }
+
/**
* Create a new file.
*
@@ -354,10 +435,15 @@
*/
public function store(Request $request)
{
+ $type = $request->input('type');
+ if ($type == "collection") {
+ return $this->createCollection($request);
+ }
+
$user = $this->guard()->user();
// 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()) {
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
@@ -366,12 +452,8 @@
$filename = $request->input('name');
$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['mimetype'] = $request->headers->get('Content-Type', null);
if ($media == 'resumable') {
$params['uploadId'] = 'resumable';
@@ -384,6 +466,15 @@
$file = $user->fsItems()->create(['type' => Item::TYPE_INCOMPLETE | Item::TYPE_FILE]);
$file->setProperty('name', $filename);
+ if ($parentHeader = $request->headers->get('X-Kolab-Parents', null)) {
+ $parents = explode(',', $parentHeader);
+ $file->parents()->attach($parents);
+ }
+
+ if ($parent = $request->input('parent')) {
+ $file->parents()->attach([$parent]);
+ }
+
try {
$response = Storage::fileInput($request->getContent(true), $params, $file);
@@ -412,12 +503,26 @@
*/
public function update(Request $request, $id)
{
- $file = $this->inputFile($id, self::WRITE);
+ $file = $this->inputItem($id, self::WRITE);
if (is_int($file)) {
return $this->errorResponse($file);
}
+
+ 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()->attach($parents);
+ }
+ if ($parentHeader = $request->headers->get('X-Kolab-Remove-Parents', null)) {
+ $parents = explode(',', $parentHeader);
+ $file->parents()->detach($parents);
+ }
+
$media = $request->input('media') ?: 'metadata';
if ($media == 'metadata') {
@@ -425,7 +530,7 @@
// Validate file name input
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()) {
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
@@ -548,7 +653,7 @@
*
* @return \App\Fs\Item|int File object or error code
*/
- protected function inputFile($fileId, $permission)
+ protected function inputItem($fileId, $permission)
{
$user = $this->guard()->user();
$isShare = str_starts_with($fileId, 'share-');
@@ -572,7 +677,7 @@
$file = Item::find($fileId);
- if (!$file || !($file->type & Item::TYPE_FILE) || ($file->type & Item::TYPE_INCOMPLETE)) {
+ if (!$file) {
return 404;
}
@@ -580,6 +685,10 @@
return 403;
}
+ if ($file->type & Item::TYPE_FILE && $file->type & Item::TYPE_INCOMPLETE) {
+ return 404;
+ }
+
return $file;
}
@@ -594,6 +703,11 @@
protected function objectToClient($object, bool $full = false): array
{
$result = ['id' => $object->id];
+ if ($object->type & ITEM::TYPE_COLLECTION) {
+ $result['type'] = 'collection';
+ } else {
+ $result['type'] = 'item';
+ }
if ($full) {
$props = array_filter($object->getProperties(['name', 'size', 'mimetype']));
diff --git a/src/app/Providers/PassportServiceProvider.php b/src/app/Providers/PassportServiceProvider.php
--- a/src/app/Providers/PassportServiceProvider.php
+++ b/src/app/Providers/PassportServiceProvider.php
@@ -20,6 +20,7 @@
Passport::tokensCan([
'api' => 'Access API',
'mfa' => 'Access MFA API',
+ 'files' => 'Access Files API',
]);
Passport::tokensExpireIn(now()->addMinutes(\config('auth.token_expiry_minutes')));
diff --git a/src/app/Rules/FileName.php b/src/app/Rules/FileName.php
--- a/src/app/Rules/FileName.php
+++ b/src/app/Rules/FileName.php
@@ -3,23 +3,10 @@
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
-use Illuminate\Support\Facades\Validator;
-use Illuminate\Support\Str;
class FileName implements Rule
{
private $message;
- private $owner;
-
- /**
- * Class constructor.
- *
- * @param \App\User $owner The file owner
- */
- public function __construct($owner)
- {
- $this->owner = $owner;
- }
/**
* Determine if the validation rule passes.
@@ -56,18 +43,6 @@
// FIXME: Should we require a dot?
- // Check if the name is unique
- $exists = $this->owner->fsItems()
- ->join('fs_properties', 'fs_items.id', '=', 'fs_properties.item_id')
- ->where('key', 'name')
- ->where('value', $name)
- ->exists();
-
- if ($exists) {
- $this->message = \trans('validation.file-name-exists');
- return false;
- }
-
return true;
}
diff --git a/src/database/migrations/2023_03_05_100000_fs_relations_table.php b/src/database/migrations/2023_03_05_100000_fs_relations_table.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2023_03_05_100000_fs_relations_table.php
@@ -0,0 +1,41 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+return new class extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create(
+ 'fs_relations',
+ function (Blueprint $table) {
+ $table->bigIncrements('id');
+ $table->string('item', 36);
+ $table->string('related', 36);
+
+ $table->foreign('item')->references('id')->on('fs_items')
+ ->onDelete('cascade');
+ $table->foreign('related')->references('id')->on('fs_items')
+ ->onDelete('cascade');
+ $table->unique(['item', 'related']);
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('fs_relations');
+ }
+};
diff --git a/src/resources/js/files.js b/src/resources/js/files.js
--- a/src/resources/js/files.js
+++ b/src/resources/js/files.js
@@ -94,7 +94,7 @@
// A "recursive" function to upload the file in chunks (if needed)
const uploadFn = (start = 0, uploadId) => {
- let url = 'api/v4/files'
+ let url = 'api/v4/items'
let body = ''
if (file.size <= maxChunkSize) {
@@ -103,15 +103,21 @@
// the file is uploading, but the risk is quite small.
body = file
start += maxChunkSize
+ if (params.parent) {
+ url = url + '?parent=' + params.parent
+ }
} else if (!uploadId) {
// The file is big, first send a request for the upload location
// The upload location does not require authentication, which means
// there should be no problem with expired auth token, etc.
config.params.media = 'resumable'
config.params.size = file.size
+ if (params.parent) {
+ url = url + '?parent=' + params.parent
+ }
} else {
// Upload a chunk of the file to the upload location
- url = 'api/v4/files/uploads/' + uploadId
+ url = 'api/v4/items/uploads/' + uploadId
body = file.slice(start, start + maxChunkSize, file.type)
config.params = { from: start }
@@ -154,7 +160,7 @@
* Download a file. Starts downloading using a hidden link trick.
*/
this.fileDownload = (id) => {
- axios.get('api/v4/files/' + id + '?downloadUrl=1')
+ axios.get('api/v4/items/' + id + '?downloadUrl=1')
.then(response => {
// Create a dummy link element and click it
if (response.data.downloadUrl) {
@@ -167,7 +173,7 @@
* Rename a file.
*/
this.fileRename = (id, name) => {
- axios.put('api/v4/files/' + id, { name })
+ axios.put('api/v4/items/' + id, { name })
.then(response => {
})
diff --git a/src/resources/js/user/routes.js b/src/resources/js/user/routes.js
--- a/src/resources/js/user/routes.js
+++ b/src/resources/js/user/routes.js
@@ -81,7 +81,7 @@
meta: { requiresAuth: true /*, perm: 'files' */ }
},
{
- path: '/files',
+ path: '/files/:parent?',
name: 'files',
component: FileListComponent,
meta: { requiresAuth: true, perm: 'files' }
diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php
--- a/src/resources/lang/en/app.php
+++ b/src/resources/lang/en/app.php
@@ -79,6 +79,10 @@
'file-permissions-update-success' => 'File permissions updated successfully.',
'file-permissions-delete-success' => 'File permissions deleted successfully.',
+ 'collection-create-success' => 'Collection created successfully.',
+ 'collection-delete-success' => 'Collection deleted successfully.',
+ 'collection-update-success' => 'Collection updated successfully.',
+
'resource-update-success' => 'Resource updated successfully.',
'resource-create-success' => 'Resource created successfully.',
'resource-delete-success' => 'Resource deleted successfully.',
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -161,6 +161,12 @@
. "to the file via a unique link.",
],
+ 'collection' => [
+ 'create' => "Create collection",
+ 'new' => "New Collection",
+ 'name' => "Name",
+ ],
+
'form' => [
'acl' => "Access rights",
'acl-full' => "All",
diff --git a/src/resources/vue/File/Info.vue b/src/resources/vue/File/Info.vue
--- a/src/resources/vue/File/Info.vue
+++ b/src/resources/vue/File/Info.vue
@@ -1,7 +1,21 @@
<template>
<div class="container">
<div class="card" id="file-info">
- <div class="card-body">
+ <div class="card-body" v-if="fileId === 'newCollection'">
+ <div class="card-title" >{{ $t('collection.new') }}</div>
+ <div class="card-text">
+ <form @submit.prevent="submit" class="card-body">
+ <div class="row mb-3">
+ <label for="name" class="col-sm-4 col-form-label">{{ $t('collection.name') }}</label>
+ <div class="col-sm-8">
+ <input type="text" class="form-control" id="name" v-model="file.name" :disabled="file.id">
+ </div>
+ </div>
+ <btn v-if="!file.id" class="btn-primary mt-3" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
+ </form>
+ </div>
+ </div>
+ <div class="card-body" v-else>
<div class="card-title">
{{ file.name }}
<btn v-if="file.canDelete" class="btn-outline-danger button-delete float-end" @click="fileDelete" icon="trash-can">{{ $t('file.delete') }}</btn>
@@ -83,6 +97,7 @@
return {
file: {},
fileId: null,
+ collectionId: null,
shares: []
}
},
@@ -90,21 +105,24 @@
this.api = new FileAPI({})
this.fileId = this.$route.params.file
+ this.collectionId = this.$route.query.parent
- axios.get('/api/v4/files/' + this.fileId, { loader: true })
- .then(response => {
- this.file = response.data
+ if (this.fileId != 'newCollection') {
+ axios.get('/api/v4/files/' + this.fileId, { loader: true })
+ .then(response => {
+ this.file = response.data
- if (this.file.isOwner) {
- axios.get('api/v4/files/' + this.fileId + '/permissions')
- .then(response => {
- if (response.data.list) {
- this.shares = response.data.list
- }
- })
- }
- })
- .catch(this.$root.errorHandler)
+ if (this.file.isOwner) {
+ axios.get('api/v4/files/' + this.fileId + '/permissions')
+ .then(response => {
+ if (response.data.list) {
+ this.shares = response.data.list
+ }
+ })
+ }
+ })
+ .catch(this.$root.errorHandler)
+ }
},
methods: {
copyLink(link) {
@@ -145,6 +163,19 @@
this.$delete(this.shares, this.shares.findIndex(element => element.id == id))
}
})
+ },
+ submit() {
+ this.$root.clearFormValidation($('#general form'))
+
+ let post = this.$root.pick(this.file, ['name'])
+ axios.post('/api/v4/items', post, { params: {
+ type: 'collection',
+ parent: this.collectionId
+ }})
+ .then(response => {
+ this.$toast.success(response.data.message)
+ this.$router.replace({ name: 'files', params: { parent: this.collectionId }})
+ })
}
}
}
diff --git a/src/resources/vue/File/List.vue b/src/resources/vue/File/List.vue
--- a/src/resources/vue/File/List.vue
+++ b/src/resources/vue/File/List.vue
@@ -2,12 +2,25 @@
<div class="container">
<div class="card" id="files">
<div class="card-body">
- <div class="card-title">
+ <div class="card-title" v-if="collectionId">
+ {{ $t('dashboard.files') + ' - ' + collection.name }}
+ <small><sup class="badge bg-primary">{{ $t('dashboard.beta') }}</sup></small>
+ <div id="drop-area" class="file-drop-area float-end">
+ <svg-icon icon="upload"></svg-icon> Click or drop file(s) here
+ </div>
+ <btn-router v-if="!$root.isDegraded()" class="float-end" :to="`/file/newCollection?parent=${collectionId}`" icon="folder">
+ {{ $t('collection.create') }}
+ </btn-router>
+ </div>
+ <div class="card-title" v-else>
{{ $t('dashboard.files') }}
<small><sup class="badge bg-primary">{{ $t('dashboard.beta') }}</sup></small>
<div id="drop-area" class="file-drop-area float-end">
<svg-icon icon="upload"></svg-icon> Click or drop file(s) here
</div>
+ <btn-router v-if="!$root.isDegraded()" class="float-end" to="/file/newCollection" icon="folder">
+ {{ $t('collection.create') }}
+ </btn-router>
</div>
<div class="card-text pt-4">
<div class="mb-2 d-flex w-100">
@@ -22,11 +35,18 @@
</thead>
<tbody>
<tr v-for="file in files" :key="file.id" @click="$root.clickRecord">
- <td class="name">
+ <td class="name" v-if="file.type === 'collection'">
+ <svg-icon icon="folder"></svg-icon>
+ <router-link :to="{ path: '/files/' + file.id }">{{ file.name }}</router-link>
+ </td>
+ <td class="name" v-else>
<svg-icon icon="file"></svg-icon>
- <router-link :to="{ path: 'file/' + file.id }">{{ file.name }}</router-link>
+ <router-link :to="{ path: '/file/' + file.id }">{{ file.name }}</router-link>
+ </td>
+ <td class="buttons" v-if="file.type === 'collection'">
+ <btn class="button-delete text-danger p-0 ms-1" @click="fileDelete(file)" icon="trash-can" :title="$t('btn.delete')"></btn>
</td>
- <td class="buttons">
+ <td class="buttons" v-else>
<btn class="button-download p-0 ms-1" @click="fileDownload(file)" icon="download" :title="$t('btn.download')"></btn>
<btn class="button-delete text-danger p-0 ms-1" @click="fileDelete(file)" icon="trash-can" :title="$t('btn.delete')"></btn>
</td>
@@ -49,24 +69,47 @@
library.add(
require('@fortawesome/free-solid-svg-icons/faFile').definition,
+ require('@fortawesome/free-solid-svg-icons/faFolder').definition,
require('@fortawesome/free-solid-svg-icons/faDownload').definition,
require('@fortawesome/free-solid-svg-icons/faUpload').definition,
)
export default {
mixins: [ ListTools ],
+ beforeRouteUpdate (to, from, next) {
+ this.collectionId = this.$route.params.parent
+ // An event called when the route that renders this component has changed,
+ // but this component is reused in the new route.
+ // Required to handle links from /file/XXX to /file/YYY
+ next()
+ this.$parent.routerReload()
+ },
data() {
return {
api: {},
- files: []
+ collection: {},
+ files: [],
+ collectionId: null
+ }
+ },
+ created() {
+ this.collectionId = this.$route.params.parent
+ if (this.collectionId) {
+ axios.get('/api/v4/items/' + this.collectionId, { loader: true })
+ .then(response => {
+ this.collection = response.data
+ })
+ .catch(this.$root.errorHandler)
}
},
mounted() {
this.uploads = {}
+ this.collectionId = this.$route.params.parent
this.api = new FileAPI({
dropArea: '#drop-area',
- eventHandler: this.eventHandler
+ eventHandler: this.eventHandler,
+ parent: this.collectionId
})
this.loadFiles({ init: true })
@@ -81,7 +124,7 @@
}
},
fileDelete(file) {
- axios.delete('api/v4/files/' + file.id)
+ axios.delete('api/v4/items/' + file.id)
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
@@ -100,7 +143,10 @@
this.api.fileDownload(file.id)
},
loadFiles(params) {
- this.listSearch('files', 'api/v4/files', params)
+ if (this.collectionId) {
+ params['parent'] = this.collectionId
+ }
+ this.listSearch('files', 'api/v4/items', params)
},
searchFiles(search) {
this.loadFiles({ reset: true, search })
diff --git a/src/resources/vue/Widgets/ListTools.vue b/src/resources/vue/Widgets/ListTools.vue
--- a/src/resources/vue/Widgets/ListTools.vue
+++ b/src/resources/vue/Widgets/ListTools.vue
@@ -99,6 +99,7 @@
data() {
return {
currentSearch: '',
+ currentParent: '',
hasMore: false,
page: 1
}
@@ -124,6 +125,14 @@
get.search = this.currentSearch
}
+
+ if ('parent' in params) {
+ get.parent = params.parent
+ this.currentParent = params.parent
+ } else {
+ get.parent = this.currentParent
+ }
+
if (!params.init) {
loader = $(this.$el).find('.more-loader')
if (!loader.length || get.page == 1) {
@@ -134,6 +143,7 @@
}
} else {
this.currentSearch = null
+ this.currentParent = null
}
axios.get(url, { params: get, loader })
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -74,6 +74,44 @@
}
);
+if (\config('app.with_files')) {
+ Route::group(
+ [
+ 'domain' => \config('app.website_domain'),
+ 'middleware' => ['auth:api', 'scope:files,api'],
+ 'prefix' => 'v4'
+ ],
+ function () {
+ Route::apiResource('files', API\V4\FsController::class);
+ Route::post('collections', [API\V4\FsController::class, 'createCollection']);
+ Route::get('files/{fileId}/permissions', [API\V4\FsController::class, 'getPermissions']);
+ Route::post('files/{fileId}/permissions', [API\V4\FsController::class, 'createPermission']);
+ Route::put('files/{fileId}/permissions/{id}', [API\V4\FsController::class, 'updatePermission']);
+ Route::delete('files/{fileId}/permissions/{id}', [API\V4\FsController::class, 'deletePermission']);
+ Route::apiResource('items', API\V4\FsController::class);
+ Route::get('items/{itemId}/permissions', [API\V4\FsController::class, 'getPermissions']);
+ Route::post('items/{itemId}/permissions', [API\V4\FsController::class, 'createPermission']);
+ Route::put('items/{itemId}/permissions/{id}', [API\V4\FsController::class, 'updatePermission']);
+ Route::delete('items/{itemId}/permissions/{id}', [API\V4\FsController::class, 'deletePermission']);
+ }
+ );
+ Route::group(
+ [
+ 'domain' => \config('app.website_domain'),
+ 'middleware' => [],
+ 'prefix' => 'v4'
+ ],
+ function () {
+ Route::post('files/uploads/{id}', [API\V4\FsController::class, 'upload'])
+ ->middleware(['api']);
+ Route::get('files/downloads/{id}', [API\V4\FsController::class, 'download']);
+ Route::post('items/uploads/{id}', [API\V4\FsController::class, 'upload'])
+ ->middleware(['api']);
+ Route::get('items/downloads/{id}', [API\V4\FsController::class, 'download']);
+ }
+ );
+}
+
Route::group(
[
'domain' => \config('app.website_domain'),
@@ -92,19 +130,6 @@
Route::get('domains/{id}/status', [API\V4\DomainsController::class, 'status']);
Route::post('domains/{id}/config', [API\V4\DomainsController::class, 'setConfig']);
- if (\config('app.with_files')) {
- Route::apiResource('files', API\V4\FilesController::class);
- Route::get('files/{fileId}/permissions', [API\V4\FilesController::class, 'getPermissions']);
- Route::post('files/{fileId}/permissions', [API\V4\FilesController::class, 'createPermission']);
- Route::put('files/{fileId}/permissions/{id}', [API\V4\FilesController::class, 'updatePermission']);
- Route::delete('files/{fileId}/permissions/{id}', [API\V4\FilesController::class, 'deletePermission']);
- Route::post('files/uploads/{id}', [API\V4\FilesController::class, 'upload'])
- ->withoutMiddleware(['auth:api', 'scope:api'])
- ->middleware(['api']);
- Route::get('files/downloads/{id}', [API\V4\FilesController::class, 'download'])
- ->withoutMiddleware(['auth:api', 'scope:api']);
- }
-
Route::apiResource('groups', API\V4\GroupsController::class);
Route::get('groups/{id}/skus', [API\V4\GroupsController::class, 'skus']);
Route::get('groups/{id}/status', [API\V4\GroupsController::class, 'status']);
diff --git a/src/tests/Feature/Controller/FilesTest.php b/src/tests/Feature/Controller/FsTest.php
rename from src/tests/Feature/Controller/FilesTest.php
rename to src/tests/Feature/Controller/FsTest.php
--- a/src/tests/Feature/Controller/FilesTest.php
+++ b/src/tests/Feature/Controller/FsTest.php
@@ -12,7 +12,7 @@
/**
* @group files
*/
-class FilesTest extends TestCase
+class FsTest extends TestCase
{
/**
* {@inheritDoc}
@@ -31,7 +31,7 @@
{
Item::query()->delete();
- $disk = LaravelStorage::disk('files');
+ $disk = LaravelStorage::disk(\config('filesystems.default'));
foreach ($disk->listContents('') as $dir) {
$disk->deleteDirectory($dir->path());
}
@@ -40,7 +40,7 @@
}
/**
- * Test deleting files (DELETE /api/v4/files/<file-id>)
+ * Test deleting items (DELETE /api/v4/items/<item-id>)
*/
public function testDelete(): void
{
@@ -49,19 +49,19 @@
$file = $this->getTestFile($john, 'teśt.txt', 'Teśt content', ['mimetype' => 'plain/text']);
// Unauth access
- $response = $this->delete("api/v4/files/{$file->id}");
+ $response = $this->delete("api/v4/items/{$file->id}");
$response->assertStatus(401);
// Unauth access
- $response = $this->actingAs($jack)->delete("api/v4/files/{$file->id}");
+ $response = $this->actingAs($jack)->delete("api/v4/items/{$file->id}");
$response->assertStatus(403);
// Non-existing file
- $response = $this->actingAs($john)->delete("api/v4/files/123");
+ $response = $this->actingAs($john)->delete("api/v4/items/123");
$response->assertStatus(404);
// File owner access
- $response = $this->actingAs($john)->delete("api/v4/files/{$file->id}");
+ $response = $this->actingAs($john)->delete("api/v4/items/{$file->id}");
$response->assertStatus(200);
$json = $response->json();
@@ -70,13 +70,13 @@
$this->assertSame("File deleted successfully.", $json['message']);
$this->assertSame(null, Item::find($file->id));
- // Note: The file is expected to stay still in the filesystem, we're not testing this here.
+ // Note: The file is expected to stay still in the itemsystem, we're not testing this here.
// TODO: Test acting as another user with permissions
}
/**
- * Test file downloads (GET /api/v4/files/downloads/<id>)
+ * Test file downloads (GET /api/v4/items/downloads/<id>)
*/
public function testDownload(): void
{
@@ -85,18 +85,18 @@
$file = $this->getTestFile($john, 'teśt.txt', 'Teśt content', ['mimetype' => 'plain/text']);
// Unauth access
- $response = $this->get("api/v4/files/{$file->id}?downloadUrl=1");
+ $response = $this->get("api/v4/items/{$file->id}?downloadUrl=1");
$response->assertStatus(401);
- $response = $this->actingAs($jack)->get("api/v4/files/{$file->id}?downloadUrl=1");
+ $response = $this->actingAs($jack)->get("api/v4/items/{$file->id}?downloadUrl=1");
$response->assertStatus(403);
// Non-existing file
- $response = $this->actingAs($john)->get("api/v4/files/123456?downloadUrl=1");
+ $response = $this->actingAs($john)->get("api/v4/items/123456?downloadUrl=1");
$response->assertStatus(404);
// Get downloadLink for the file
- $response = $this->actingAs($john)->get("api/v4/files/{$file->id}?downloadUrl=1");
+ $response = $this->actingAs($john)->get("api/v4/items/{$file->id}?downloadUrl=1");
$response->assertStatus(200);
$json = $response->json();
@@ -108,14 +108,13 @@
$response = $this->get(substr($link, strpos($link, '/api/') + 1));
$response->assertStatus(200)
->assertHeader('Content-Disposition', "attachment; filename=test.txt; filename*=utf-8''te%C5%9Bt.txt")
- ->assertHeader('Content-Length', $file->getProperty('size'))
->assertHeader('Content-Type', $file->getProperty('mimetype'));
$this->assertSame('Teśt content', $response->streamedContent());
// Test acting as another user with read permission
$permission = $this->getTestFilePermission($file, $jack, 'r');
- $response = $this->actingAs($jack)->get("api/v4/files/{$permission->key}?downloadUrl=1");
+ $response = $this->actingAs($jack)->get("api/v4/items/{$permission->key}?downloadUrl=1");
$response->assertStatus(200);
$json = $response->json();
@@ -127,14 +126,13 @@
$response = $this->get(substr($link, strpos($link, '/api/') + 1));
$response->assertStatus(200)
->assertHeader('Content-Disposition', "attachment; filename=test.txt; filename*=utf-8''te%C5%9Bt.txt")
- ->assertHeader('Content-Length', $file->getProperty('size'))
->assertHeader('Content-Type', $file->getProperty('mimetype'));
$this->assertSame('Teśt content', $response->streamedContent());
// Test downloading a multi-chunk file
$file = $this->getTestFile($john, 'test2.txt', ['T1', 'T2'], ['mimetype' => 'plain/text']);
- $response = $this->actingAs($john)->get("api/v4/files/{$file->id}?downloadUrl=1");
+ $response = $this->actingAs($john)->get("api/v4/items/{$file->id}?downloadUrl=1");
$response->assertStatus(200);
$json = $response->json();
@@ -146,14 +144,13 @@
$response = $this->get(substr($link, strpos($link, '/api/') + 1));
$response->assertStatus(200)
->assertHeader('Content-Disposition', "attachment; filename=test2.txt")
- ->assertHeader('Content-Length', $file->getProperty('size'))
->assertHeader('Content-Type', $file->getProperty('mimetype'));
$this->assertSame('T1T2', $response->streamedContent());
}
/**
- * Test fetching/creating/updaing/deleting file permissions (GET|POST|PUT /api/v4/files/<file-id>/permissions)
+ * Test fetching/creating/updaing/deleting file permissions (GET|POST|PUT /api/v4/items/<file-id>/permissions)
*/
public function testPermissions(): void
{
@@ -162,25 +159,25 @@
$file = $this->getTestFile($john, 'test1.txt', []);
// Unauth access not allowed
- $response = $this->get("api/v4/files/{$file->id}/permissions");
+ $response = $this->get("api/v4/items/{$file->id}/permissions");
$response->assertStatus(401);
- $response = $this->post("api/v4/files/{$file->id}/permissions", []);
+ $response = $this->post("api/v4/items/{$file->id}/permissions", []);
$response->assertStatus(401);
// Non-existing file
- $response = $this->actingAs($john)->get("api/v4/files/1234/permissions");
+ $response = $this->actingAs($john)->get("api/v4/items/1234/permissions");
$response->assertStatus(404);
- $response = $this->actingAs($john)->post("api/v4/files/1234/permissions", []);
+ $response = $this->actingAs($john)->post("api/v4/items/1234/permissions", []);
$response->assertStatus(404);
// No permissions to the file
- $response = $this->actingAs($jack)->get("api/v4/files/{$file->id}/permissions");
+ $response = $this->actingAs($jack)->get("api/v4/items/{$file->id}/permissions");
$response->assertStatus(403);
- $response = $this->actingAs($jack)->post("api/v4/files/{$file->id}/permissions", []);
+ $response = $this->actingAs($jack)->post("api/v4/items/{$file->id}/permissions", []);
$response->assertStatus(403);
// Expect an empty list of permissions
- $response = $this->actingAs($john)->get("api/v4/files/{$file->id}/permissions");
+ $response = $this->actingAs($john)->get("api/v4/items/{$file->id}/permissions");
$response->assertStatus(200);
$json = $response->json();
@@ -190,7 +187,7 @@
$this->assertSame(0, $json['count']);
// Empty input
- $response = $this->actingAs($john)->post("api/v4/files/{$file->id}/permissions", []);
+ $response = $this->actingAs($john)->post("api/v4/items/{$file->id}/permissions", []);
$response->assertStatus(422);
$json = $response->json();
@@ -202,7 +199,7 @@
// Test more input validation
$post = ['user' => 'user', 'permissions' => 'read'];
- $response = $this->actingAs($john)->post("api/v4/files/{$file->id}/permissions", $post);
+ $response = $this->actingAs($john)->post("api/v4/items/{$file->id}/permissions", $post);
$response->assertStatus(422);
$json = $response->json();
@@ -214,7 +211,7 @@
// Let's add some permission
$post = ['user' => 'jack@kolab.org', 'permissions' => 'read-only'];
- $response = $this->actingAs($john)->post("api/v4/files/{$file->id}/permissions", $post);
+ $response = $this->actingAs($john)->post("api/v4/items/{$file->id}/permissions", $post);
$response->assertStatus(200);
$json = $response->json();
@@ -232,7 +229,7 @@
// Error handling on use of the same user
$post = ['user' => 'jack@kolab.org', 'permissions' => 'read-only'];
- $response = $this->actingAs($john)->post("api/v4/files/{$file->id}/permissions", $post);
+ $response = $this->actingAs($john)->post("api/v4/items/{$file->id}/permissions", $post);
$response->assertStatus(422);
$json = $response->json();
@@ -241,11 +238,11 @@
$this->assertSame("File permission already exists.", $json['errors']['user']);
// Test update
- $response = $this->actingAs($john)->put("api/v4/files/{$file->id}/permissions/1234", $post);
+ $response = $this->actingAs($john)->put("api/v4/items/{$file->id}/permissions/1234", $post);
$response->assertStatus(404);
$post = ['user' => 'jack@kolab.org', 'permissions' => 'read-write'];
- $response = $this->actingAs($john)->put("api/v4/files/{$file->id}/permissions/{$permission->key}", $post);
+ $response = $this->actingAs($john)->put("api/v4/items/{$file->id}/permissions/{$permission->key}", $post);
$response->assertStatus(200);
$json = $response->json();
@@ -263,7 +260,7 @@
// Input validation on update
$post = ['user' => 'jack@kolab.org', 'permissions' => 'read'];
- $response = $this->actingAs($john)->put("api/v4/files/{$file->id}/permissions/{$permission->key}", $post);
+ $response = $this->actingAs($john)->put("api/v4/items/{$file->id}/permissions/{$permission->key}", $post);
$response->assertStatus(422);
$json = $response->json();
@@ -272,7 +269,7 @@
$this->assertSame("The file permission is invalid.", $json['errors']['permissions']);
// Test GET with existing permissions
- $response = $this->actingAs($john)->get("api/v4/files/{$file->id}/permissions");
+ $response = $this->actingAs($john)->get("api/v4/items/{$file->id}/permissions");
$response->assertStatus(200);
$json = $response->json();
@@ -287,10 +284,10 @@
$this->assertSame(\App\Utils::serviceUrl('file/' . $permission->key), $json['list'][0]['link']);
// Delete permission
- $response = $this->actingAs($john)->delete("api/v4/files/{$file->id}/permissions/1234");
+ $response = $this->actingAs($john)->delete("api/v4/items/{$file->id}/permissions/1234");
$response->assertStatus(404);
- $response = $this->actingAs($john)->delete("api/v4/files/{$file->id}/permissions/{$permission->key}");
+ $response = $this->actingAs($john)->delete("api/v4/items/{$file->id}/permissions/{$permission->key}");
$response->assertStatus(200);
$json = $response->json();
@@ -302,18 +299,18 @@
}
/**
- * Test fetching files/folders list (GET /api/v4/files)
+ * Test fetching items/folders list (GET /api/v4/items)
*/
public function testIndex(): void
{
// Unauth access not allowed
- $response = $this->get("api/v4/files");
+ $response = $this->get("api/v4/items");
$response->assertStatus(401);
$user = $this->getTestUser('john@kolab.org');
// Expect an empty list
- $response = $this->actingAs($user)->get("api/v4/files");
+ $response = $this->actingAs($user)->get("api/v4/items");
$response->assertStatus(200);
$json = $response->json();
@@ -323,11 +320,11 @@
$this->assertSame(0, $json['count']);
$this->assertSame(false, $json['hasMore']);
- // Create some files and test again
+ // Create some items and test again
$file1 = $this->getTestFile($user, 'test1.txt', [], ['mimetype' => 'text/plain', 'size' => 12345]);
$file2 = $this->getTestFile($user, 'test2.gif', [], ['mimetype' => 'image/gif', 'size' => 10000]);
- $response = $this->actingAs($user)->get("api/v4/files");
+ $response = $this->actingAs($user)->get("api/v4/items");
$response->assertStatus(200);
$json = $response->json();
@@ -342,7 +339,7 @@
$this->assertSame($file2->id, $json['list'][1]['id']);
// Searching
- $response = $this->actingAs($user)->get("api/v4/files?search=t2");
+ $response = $this->actingAs($user)->get("api/v4/items?search=t2");
$response->assertStatus(200);
$json = $response->json();
@@ -356,11 +353,11 @@
// TODO: Test paging
- // Make sure incomplete files are skipped
+ // Make sure incomplete items are skipped
$file1->type |= Item::TYPE_INCOMPLETE;
$file1->save();
- $response = $this->actingAs($user)->get("api/v4/files");
+ $response = $this->actingAs($user)->get("api/v4/items");
$response->assertStatus(200);
$json = $response->json();
@@ -370,12 +367,12 @@
}
/**
- * Test fetching file metadata (GET /api/v4/files/<file-id>)
+ * Test fetching file metadata (GET /api/v4/items/<file-id>)
*/
public function testShow(): void
{
// Unauth access not allowed
- $response = $this->get("api/v4/files/1234");
+ $response = $this->get("api/v4/items/1234");
$response->assertStatus(401);
$john = $this->getTestUser('john@kolab.org');
@@ -383,15 +380,15 @@
$file = $this->getTestFile($john, 'teśt.txt', 'Teśt content', ['mimetype' => 'plain/text']);
// Non-existing file
- $response = $this->actingAs($jack)->get("api/v4/files/1234");
+ $response = $this->actingAs($jack)->get("api/v4/items/1234");
$response->assertStatus(404);
// Unauthorized access
- $response = $this->actingAs($jack)->get("api/v4/files/{$file->id}");
+ $response = $this->actingAs($jack)->get("api/v4/items/{$file->id}");
$response->assertStatus(403);
// Get file metadata
- $response = $this->actingAs($john)->get("api/v4/files/{$file->id}");
+ $response = $this->actingAs($john)->get("api/v4/items/{$file->id}");
$response->assertStatus(200);
$json = $response->json();
@@ -405,17 +402,16 @@
$this->assertSame(true, $json['canDelete']);
// Get file content
- $response = $this->actingAs($john)->get("api/v4/files/{$file->id}?download=1");
+ $response = $this->actingAs($john)->get("api/v4/items/{$file->id}?download=1");
$response->assertStatus(200)
->assertHeader('Content-Disposition', "attachment; filename=test.txt; filename*=utf-8''te%C5%9Bt.txt")
- ->assertHeader('Content-Length', $file->getProperty('size'))
->assertHeader('Content-Type', $file->getProperty('mimetype'));
$this->assertSame('Teśt content', $response->streamedContent());
// Test acting as a user with file permissions
$permission = $this->getTestFilePermission($file, $jack, 'r');
- $response = $this->actingAs($jack)->get("api/v4/files/{$permission->key}");
+ $response = $this->actingAs($jack)->get("api/v4/items/{$permission->key}");
$response->assertStatus(200);
$json = $response->json();
@@ -427,18 +423,18 @@
}
/**
- * Test creating files (POST /api/v4/files)
+ * Test creating items (POST /api/v4/items)
*/
public function testStore(): void
{
// Unauth access not allowed
- $response = $this->post("api/v4/files");
+ $response = $this->post("api/v4/items");
$response->assertStatus(401);
$john = $this->getTestUser('john@kolab.org');
// Test input validation
- $response = $this->sendRawBody($john, 'POST', "api/v4/files", [], '');
+ $response = $this->sendRawBody($john, 'POST', "api/v4/items", [], '');
$response->assertStatus(422);
$json = $response->json();
@@ -446,7 +442,7 @@
$this->assertSame('error', $json['status']);
$this->assertSame(["The name field is required."], $json['errors']['name']);
- $response = $this->sendRawBody($john, 'POST', "api/v4/files?name=*.txt", [], '');
+ $response = $this->sendRawBody($john, 'POST', "api/v4/items?name=*.txt", [], '');
$response->assertStatus(422);
$json = $response->json();
@@ -457,7 +453,7 @@
// Create a file - the simple method
$body = "test content";
$headers = [];
- $response = $this->sendRawBody($john, 'POST', "api/v4/files?name=test.txt", $headers, $body);
+ $response = $this->sendRawBody($john, 'POST', "api/v4/items?name=test.txt", $headers, $body);
$response->assertStatus(200);
$json = $response->json();
@@ -478,13 +474,13 @@
}
/**
- * Test creating files - resumable (POST /api/v4/files)
+ * Test creating items - resumable (POST /api/v4/items)
*/
public function testStoreResumable(): void
{
$john = $this->getTestUser('john@kolab.org');
- $response = $this->actingAs($john)->post("api/v4/files?name=test2.txt&media=resumable&size=400");
+ $response = $this->actingAs($john)->post("api/v4/items?name=test2.txt&media=resumable&size=400");
$response->assertStatus(200);
$json = $response->json();
@@ -501,7 +497,7 @@
for ($x = 0; $x <= 2; $x++) {
$body = str_repeat("$x", 100);
- $response = $this->sendRawBody(null, 'POST', "api/v4/files/uploads/{$uploadId}?from={$size}", [], $body);
+ $response = $this->sendRawBody(null, 'POST', "api/v4/items/uploads/{$uploadId}?from={$size}", [], $body);
$response->assertStatus(200);
$json = $response->json();
@@ -513,7 +509,7 @@
}
$body = str_repeat("$x", 100);
- $response = $this->sendRawBody(null, 'POST', "api/v4/files/uploads/{$uploadId}?from={$size}", [], $body);
+ $response = $this->sendRawBody(null, 'POST', "api/v4/items/uploads/{$uploadId}?from={$size}", [], $body);
$response->assertStatus(200);
$json = $response->json();
@@ -536,12 +532,12 @@
}
/**
- * Test updating files (PUT /api/v4/files/<file-id>)
+ * Test updating items (PUT /api/v4/items/<file-id>)
*/
public function testUpdate(): void
{
// Unauth access not allowed
- $response = $this->put("api/v4/files/1234");
+ $response = $this->put("api/v4/items/1234");
$response->assertStatus(401);
$john = $this->getTestUser('john@kolab.org');
@@ -549,16 +545,16 @@
$file = $this->getTestFile($john, 'teśt.txt', 'Teśt content', ['mimetype' => 'plain/text']);
// Non-existing file
- $response = $this->actingAs($john)->put("api/v4/files/1234", []);
+ $response = $this->actingAs($john)->put("api/v4/items/1234", []);
$response->assertStatus(404);
// Unauthorized access
- $response = $this->actingAs($jack)->put("api/v4/files/{$file->id}", []);
+ $response = $this->actingAs($jack)->put("api/v4/items/{$file->id}", []);
$response->assertStatus(403);
// Test name validation
$post = ['name' => 'test/test.txt'];
- $response = $this->actingAs($john)->put("api/v4/files/{$file->id}", $post);
+ $response = $this->actingAs($john)->put("api/v4/items/{$file->id}", $post);
$response->assertStatus(422);
$json = $response->json();
@@ -567,7 +563,7 @@
$this->assertSame(["The file name is invalid."], $json['errors']['name']);
$post = ['name' => 'new name.txt', 'media' => 'test'];
- $response = $this->actingAs($john)->put("api/v4/files/{$file->id}", $post);
+ $response = $this->actingAs($john)->put("api/v4/items/{$file->id}", $post);
$response->assertStatus(422);
$json = $response->json();
@@ -577,7 +573,7 @@
// Rename a file
$post = ['name' => 'new namś.txt'];
- $response = $this->actingAs($john)->put("api/v4/files/{$file->id}", $post);
+ $response = $this->actingAs($john)->put("api/v4/items/{$file->id}", $post);
$response->assertStatus(200);
$json = $response->json();
@@ -591,7 +587,7 @@
// Update file content
$body = "Test1\nTest2";
- $response = $this->sendRawBody($john, 'PUT', "api/v4/files/{$file->id}?media=content", [], $body);
+ $response = $this->sendRawBody($john, 'PUT', "api/v4/items/{$file->id}?media=content", [], $body);
$response->assertStatus(200);
$json = $response->json();
@@ -621,7 +617,7 @@
*/
protected function getTestFile(User $user, string $name, $content = [], $props = []): Item
{
- $disk = LaravelStorage::disk('files');
+ $disk = LaravelStorage::disk(\config('filesystems.default'));
$file = $user->fsItems()->create(['type' => Item::TYPE_FILE]);
$size = 0;
@@ -656,6 +652,28 @@
return $file;
}
+ /**
+ * Create a test collection.
+ *
+ * @param \App\User $user File owner
+ * @param string $name File name
+ * @param array $props Extra collection properties
+ *
+ * @return \App\Fs\Item
+ */
+ protected function getTestCollection(User $user, string $name, $props = []): Item
+ {
+ $collection = $user->fsItems()->create(['type' => Item::TYPE_COLLECTION]);
+
+ $properties = [
+ 'name' => $name,
+ ];
+
+ $collection->setProperties($props + $properties);
+
+ return $collection;
+ }
+
/**
* Get contents of a test file.
*
@@ -668,7 +686,7 @@
$content = '';
$file->chunks()->orderBy('sequence')->get()->each(function ($chunk) use ($file, &$content) {
- $disk = LaravelStorage::disk('files');
+ $disk = LaravelStorage::disk(\config('filesystems.default'));
$path = Storage::chunkLocation($chunk->chunk_id, $file);
$content .= $disk->read($path);
@@ -721,4 +739,226 @@
return $this->call($method, $uri, [], $cookies, [], $server, $content);
}
}
+
+
+ /**
+ * Test creating collections (POST /api/v4/items?type=collection)
+ */
+ public function testStoreCollection(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+
+ $params = [
+ 'name' => "MyTestCollection",
+ 'deviceId' => "myDeviceId",
+ ];
+ $response = $this->actingAs($john)->post("api/v4/items?type=collection", $params);
+
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+
+ $collection = Item::find($json['id']);
+
+ $this->assertSame(Item::TYPE_COLLECTION, $collection->type);
+ $this->assertSame($params['name'], $collection->getProperty('name'));
+ $this->assertSame($params['deviceId'], $collection->getProperty('deviceId'));
+ }
+
+ /**
+ * Test creating collections (POST /api/v4/items?type=collection)
+ */
+ public function testStoreCollectionMetadata(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+
+ $params = [
+ 'name' => "MyTestCollection",
+ 'deviceId' => "myDeviceId",
+ 'collectionType' => "photoalbum",
+ 'deduplicate-property' => "localId",
+ 'deduplicate-value' => "myDeviceId:localId",
+ 'property-localId' => "myDeviceId:localId",
+ ];
+ $response = $this->actingAs($john)->post("api/v4/items?type=collection", $params);
+
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+
+ $collection = Item::find($json['id']);
+
+ $this->assertSame(Item::TYPE_COLLECTION, $collection->type);
+ $this->assertSame($params['name'], $collection->getProperty('name'));
+ $this->assertSame($params['deviceId'], $collection->getProperty('deviceId'));
+ $this->assertSame($params['collectionType'], $collection->getProperty('collectionType'));
+ $this->assertSame($params['property-localId'], $collection->getProperty('localId'));
+
+
+ $params = [
+ 'name' => "MyTestCollection2",
+ 'deviceId' => "myDeviceId",
+ 'collectionType' => "photoalbum",
+ 'deduplicate-property' => "localId",
+ 'deduplicate-value' => "myDeviceId:localId",
+ ];
+ $response = $this->actingAs($john)->post("api/v4/items?type=collection", $params);
+
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame($collection->id, $json['id']);
+ $this->assertSame($params['name'], $collection->getProperty('name'));
+ }
+
+ /**
+ * Test deleting items (DELETE /api/v4/collections/<collection-id>)
+ */
+ public function testDeleteCollection(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $collection = $this->getTestCollection($john, 'Teśt content');
+
+ // File owner access
+ $response = $this->actingAs($john)->delete("api/v4/items/{$collection->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Collection deleted successfully.", $json['message']);
+ $this->assertSame(null, Item::find($collection->id));
+ }
+
+ /**
+ * Test store item relations (POST /api/v4/items)
+ */
+ public function testStoreRelation(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $collection = $this->getTestCollection($john, 'My Test Collection');
+
+ $body = "test content";
+ $headers = ["X-Kolab-Parents" => implode(',', [$collection->id])];
+ $response = $this->sendRawBody($john, 'POST', "api/v4/items?name=test.txt", $headers, $body);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+ $newItem = Item::find($json['id']);
+ $this->assertNotNull($newItem);
+ $this->assertSame(1, $newItem->parents()->count());
+ $this->assertSame($collection->id, $newItem->parents()->first()->id);
+
+
+ $collection2 = $this->getTestCollection($john, 'My Test Collection2');
+ $headers = ["X-Kolab-Parents" => implode(',', [$collection->id, $collection2->id])];
+ $response = $this->sendRawBody($john, 'POST', "api/v4/items?name=test2.txt", $headers, $body);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+ $newItem = Item::find($json['id']);
+ $this->assertNotNull($newItem);
+ $this->assertSame(2, $newItem->parents()->count());
+ }
+
+ /**
+ * Test store item relations (POST /api/v4/items)
+ */
+ public function testStoreRelationParameter(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $collection = $this->getTestCollection($john, 'My Test Collection');
+
+ $body = "test content";
+ $response = $this->sendRawBody($john, 'POST', "api/v4/items?name=test.txt&parent={$collection->id}", [], $body);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+ $newItem = Item::find($json['id']);
+ $this->assertNotNull($newItem);
+ $this->assertSame(1, $newItem->parents()->count());
+ $this->assertSame($collection->id, $newItem->parents()->first()->id);
+ }
+
+ /**
+ * Test update item relations (PUT /api/v4/items/$itemid)
+ * Add/Remove/Set
+ */
+ public function testUpdateRelation(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $file = $this->getTestFile($john, 'test1.txt', 'Teśt content1', ['mimetype' => 'plain/text']);
+ $collection1 = $this->getTestCollection($john, 'My Test Collection');
+ $collection2 = $this->getTestCollection($john, 'My Test Collection2');
+
+ // Add parents
+ $headers = ["X-Kolab-Add-Parents" => implode(',', [$collection1->id])];
+ $response = $this->sendRawBody($john, 'PUT', "api/v4/items/{$file->id}", $headers, '');
+ $response->assertStatus(200);
+ $this->assertSame('success', $response->json()['status']);
+
+ $parents = $file->parents()->get();
+ $this->assertSame(1, count($parents));
+ $this->assertSame($collection1->id, $parents->first()->id);
+
+ // Set parents
+ $headers = ["X-Kolab-Parents" => implode(',', [$collection1->id, $collection2->id])];
+ $response = $this->sendRawBody($john, 'PUT', "api/v4/items/{$file->id}", $headers, '');
+ $response->assertStatus(200);
+ $this->assertSame('success', $response->json()['status']);
+
+ $parents = $file->parents()->get();
+ $this->assertSame(2, count($parents));
+
+ // Remove parents
+ $headers = ["X-Kolab-Remove-Parents" => implode(',', [$collection1->id])];
+ $response = $this->sendRawBody($john, 'PUT', "api/v4/items/{$file->id}", $headers, '');
+ $response->assertStatus(200);
+ $this->assertSame('success', $response->json()['status']);
+
+ $parents = $file->parents()->get();
+ $this->assertSame(1, count($parents));
+ $this->assertSame($collection2->id, $parents->first()->id);
+ }
+
+ public function testListChildren(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $file1 = $this->getTestFile($john, 'test1.txt', 'Teśt content1', ['mimetype' => 'plain/text']);
+ $file2 = $this->getTestFile($john, 'test2.txt', 'Teśt content2', ['mimetype' => 'plain/text']);
+ $collection = $this->getTestCollection($john, 'My Test Collection');
+ $collection->children()->attach($file1);
+
+ // List files in collection
+ $response = $this->actingAs($john)->get("api/v4/items?parent={$collection->id}");
+ $response->assertStatus(200);
+ $json = $response->json();
+
+ $list = $json['list'];
+ $this->assertSame(1, count($list));
+ $this->assertSame($file1->id, $list[0]['id']);
+
+ // List files not in a collection
+ $response = $this->actingAs($john)->get("api/v4/items?type=file");
+ $response->assertStatus(200);
+ $json = $response->json();
+
+ $list = $json['list'];
+ $this->assertSame(1, count($list));
+ $this->assertSame($file2->id, $list[0]['id']);
+
+ // Remove from collection
+ $collection->children()->detach($file1);
+
+ $response = $this->actingAs($john)->get("api/v4/items?parent={$collection->id}");
+ $response->assertStatus(200);
+ $json = $response->json();
+ $this->assertSame(0, count($response->json()['list']));
+ }
}

File Metadata

Mime Type
text/plain
Expires
Mon, Apr 6, 5:19 AM (12 h, 55 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18834922
Default Alt Text
D4190.1775452770.diff (66 KB)

Event Timeline