Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117775973
D4190.1775242625.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
67 KB
Referenced Files
None
Subscribers
None
D4190.1775242625.diff
View Options
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,63 @@
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)
+ {
+ $item = $this->deduplicateOrCreate($request, Item::TYPE_COLLECTION);
+ $item->setProperties([
+ 'name' => $request->input('name'),
+ 'deviceId' => $request->input('deviceId'),
+ 'collectionType' => $request->input('collectionType'),
+ ]);
+
+ foreach ($request->all() as $key => $value) {
+ if (str_starts_with($key, "property-")) {
+ $item->setProperty(substr($key, 9), $value);
+ }
+ }
+
+ if ($parent = $request->input('parent')) {
+ $item->parents()->sync([$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 +438,13 @@
*/
public function store(Request $request)
{
- $user = $this->guard()->user();
+ $type = $request->input('type');
+ if ($type == "collection") {
+ return $this->createCollection($request);
+ }
// 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 +453,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';
@@ -380,10 +463,24 @@
}
// TODO: Delete the existing incomplete file with the same name?
-
- $file = $user->fsItems()->create(['type' => Item::TYPE_INCOMPLETE | Item::TYPE_FILE]);
+ $file = $this->deduplicateOrCreate($request, Item::TYPE_FILE);
$file->setProperty('name', $filename);
+ foreach ($request->all() as $key => $value) {
+ if (str_starts_with($key, "property-")) {
+ $file->setProperty(substr($key, 9), $value);
+ }
+ }
+
+ 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 {
$response = Storage::fileInput($request->getContent(true), $params, $file);
@@ -412,12 +509,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()->syncWithoutDetaching($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 +536,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 +659,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 +683,7 @@
$file = Item::find($fileId);
- if (!$file || !($file->type & Item::TYPE_FILE) || ($file->type & Item::TYPE_INCOMPLETE)) {
+ if (!$file) {
return 404;
}
@@ -580,6 +691,10 @@
return 403;
}
+ if ($file->type & Item::TYPE_FILE && $file->type & Item::TYPE_INCOMPLETE) {
+ return 404;
+ }
+
return $file;
}
@@ -594,6 +709,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(): void
+ {
+ 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(): void
+ {
+ 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
@@ -82,7 +82,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.',
+
'payment-status-paid' => 'The payment has been completed successfully.',
'payment-status-canceled' => 'The payment has been canceled.',
'payment-status-failed' => 'The payment failed.',
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
@@ -162,6 +162,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
@@ -75,6 +75,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'),
@@ -93,19 +131,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,237 @@
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'));
+
+ // Deduplicate but update the name and parent
+ $parent = $this->getTestCollection($john, 'Parent');
+ $params = [
+ 'name' => "MyTestCollection2",
+ 'deviceId' => "myDeviceId",
+ 'parent' => $parent->id,
+ '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'));
+
+
+ // Deduplicate again, but without changes
+ $parent = $this->getTestCollection($john, 'Parent');
+ $response = $this->actingAs($john)->post("api/v4/items?type=collection", $params);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+ $this->assertSame('success', $json['status']);
+ }
+
+ /**
+ * 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
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 3, 6:57 PM (2 d, 2 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18825773
Default Alt Text
D4190.1775242625.diff (67 KB)
Attached To
Mode
D4190: Collection support for filesystem items
Attached
Detach File
Event Timeline