Page MenuHomePhorge

D4364.1775833730.diff
No OneTemporary

Authored By
Unknown
Size
48 KB
Referenced Files
None
Subscribers
None

D4364.1775833730.diff

diff --git a/src/app/Http/Controllers/API/V4/FsController.php b/src/app/Http/Controllers/API/V4/FsController.php
--- a/src/app/Http/Controllers/API/V4/FsController.php
+++ b/src/app/Http/Controllers/API/V4/FsController.php
@@ -11,6 +11,7 @@
use App\Utils;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
class FsController extends RelationController
@@ -50,15 +51,12 @@
$file->delete();
if ($file->type & Item::TYPE_COLLECTION) {
- return response()->json([
- 'status' => 'success',
- 'message' => self::trans('app.collection-delete-success'),
- ]);
+ $message = self::trans('app.collection-delete-success');
}
return response()->json([
'status' => 'success',
- 'message' => self::trans('app.file-delete-success'),
+ 'message' => $message ?? self::trans('app.file-delete-success'),
]);
}
@@ -376,92 +374,6 @@
return response()->json($response);
}
- private function deduplicateOrCreate(Request $request, $type)
- {
- $user = $this->guard()->user();
- $item = null;
- if ($request->has('deduplicate-property')) {
- //query for item by deduplicate-value
- $result = $user->fsItems()->select('fs_items.*');
- $result->join('fs_properties', function ($join) use ($request) {
- $join->on('fs_items.id', '=', 'fs_properties.item_id')
- ->where('fs_properties.key', $request->input('deduplicate-property'));
- })
- ->where('type', '&', $type);
-
- $result->whereLike('fs_properties.value', $request->input('deduplicate-value'));
- $item = $result->first();
- }
-
- if (!$item) {
- $item = $user->fsItems()->create(['type' => $type]);
- }
- return $item;
- }
-
- /**
- * Create a new collection.
- *
- * @param \Illuminate\Http\Request $request The API request.
- *
- * @return \Illuminate\Http\JsonResponse The response
- */
- private function createCollection(Request $request)
- {
- // Validate file name input
- $v = Validator::make($request->all(), [
- 'name' => ['required', new FileName()],
- 'deviceId' => ['max:255'],
- 'collectionType' => ['max:255'],
- ]);
-
- if ($v->fails()) {
- return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
- }
-
- $properties = [
- 'name' => $request->input('name'),
- 'deviceId' => $request->input('deviceId'),
- 'collectionType' => $request->input('collectionType'),
- ];
-
- foreach ($request->all() as $key => $value) {
- if (str_starts_with($key, "property-")) {
- $propertyKey = substr($key, 9);
- if (strlen($propertyKey) > 191) {
- return response()->json([
- 'status' => 'error',
- 'errors' => [self::trans('validation.max.string', ['attribute' => $propertyKey, 'max' => 191])]
- ], 422);
- }
- if (!preg_match('/^[a-zA-Z0-9_-]+$/', $propertyKey)) {
- return response()->json([
- 'status' => 'error',
- 'errors' => [self::trans('validation.regex_format', [
- 'attribute' => $propertyKey,
- 'format' => "a-zA-Z0-9_-"
- ])]
- ], 422);
- }
- $properties[$propertyKey] = $value;
- }
- }
-
- $item = $this->deduplicateOrCreate($request, Item::TYPE_COLLECTION);
- $item->setProperties($properties);
-
- if ($parent = $request->input('parent')) {
- $item->parents()->sync([$parent]);
- }
-
- $response = [];
- $response['status'] = 'success';
- $response['id'] = $item->id;
- $response['message'] = self::trans('app.collection-create-success');
-
- return response()->json($response);
- }
-
/**
* Create a new file.
*
@@ -483,53 +395,47 @@
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
}
+ $parents = $this->getInputParents($request);
+ if ($errorResponse = $this->validateParents($parents)) {
+ return $errorResponse;
+ }
+
$filename = $request->input('name');
$media = $request->input('media');
- $params = [];
- $params['mimetype'] = $request->headers->get('Content-Type', null);
-
- if ($media == 'resumable') {
- $params['uploadId'] = 'resumable';
- $params['size'] = $request->input('size');
- $params['from'] = $request->input('from') ?: 0;
- }
-
// TODO: Delete the existing incomplete file with the same name?
$properties = ['name' => $filename];
foreach ($request->all() as $key => $value) {
- if (str_starts_with($key, "property-")) {
+ if (str_starts_with($key, 'property-')) {
$propertyKey = substr($key, 9);
- if (strlen($propertyKey) > 191) {
- return response()->json([
- 'status' => 'error',
- 'errors' => [self::trans('validation.max.string', ['attribute' => $propertyKey, 'max' => 191])]
- ], 422);
- }
- if (!preg_match('/^[a-zA-Z0-9_-]+$/', $propertyKey)) {
- return response()->json([
- 'status' => 'error',
- 'errors' => [self::trans('validation.regex_format', [
- 'attribute' => $propertyKey,
- 'format' => "a-zA-Z0-9_-"
- ])]
- ], 422);
+
+ if ($errorResponse = $this->validatePropertyName($propertyKey)) {
+ return $errorResponse;
}
+
$properties[$propertyKey] = $value;
}
}
+ DB::beginTransaction();
+
$file = $this->deduplicateOrCreate($request, Item::TYPE_INCOMPLETE | Item::TYPE_FILE);
$file->setProperties($properties);
- if ($parentHeader = $request->headers->get('X-Kolab-Parents', null)) {
- $parents = explode(',', $parentHeader);
+ if (!empty($parents)) {
$file->parents()->sync($parents);
}
- if ($parent = $request->input('parent')) {
- $file->parents()->sync([$parent]);
+ DB::commit();
+
+ $params = [];
+ $params['mimetype'] = $request->headers->get('Content-Type', null);
+
+ if ($media == 'resumable') {
+ $params['uploadId'] = 'resumable';
+ $params['size'] = $request->input('size');
+ $params['from'] = $request->input('from') ?: 0;
}
try {
@@ -566,6 +472,11 @@
return $this->errorResponse($file);
}
+ if ($file->type == self::TYPE_COLLECTION) {
+ // Updating a collection is not supported yet
+ return $this->errorResponse(405);
+ }
+
$media = $request->input('media') ?: 'metadata';
if ($media == 'metadata') {
@@ -578,25 +489,42 @@
if ($v->fails()) {
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
}
+ }
- $file->setProperty('name', $filename);
+ $parents = [
+ 'X-Kolab-Parents' => [],
+ 'X-Kolab-Add-Parents' => [],
+ 'X-Kolab-Remove-Parents' => [],
+ ];
+
+ // Collect and validate parents from the request headers
+ foreach (array_keys($parents) as $header) {
+ if ($value = $request->headers->get($header, null)) {
+ $list = explode(',', $value);
+ if ($errorResponse = $this->validateParents($list)) {
+ return $errorResponse;
+ }
+ $parents[$header] = $list;
+ }
}
+ DB::beginTransaction();
- if ($parentHeader = $request->headers->get('X-Kolab-Parents', null)) {
- $parents = explode(',', $parentHeader);
- $file->parents()->sync($parents);
+ if (count($parents['X-Kolab-Parents'])) {
+ $file->parents()->sync($parents['X-Kolab-Parents']);
}
- if ($parentHeader = $request->headers->get('X-Kolab-Add-Parents', null)) {
- $parents = explode(',', $parentHeader);
- $file->parents()->syncWithoutDetaching($parents);
+ if (count($parents['X-Kolab-Add-Parents'])) {
+ $file->parents()->syncWithoutDetaching($parents['X-Kolab-Add-Parents']);
}
- if ($parentHeader = $request->headers->get('X-Kolab-Remove-Parents', null)) {
- $parents = explode(',', $parentHeader);
- $file->parents()->detach($parents);
+ if (count($parents['X-Kolab-Remove-Parents'])) {
+ $file->parents()->detach($parents['X-Kolab-Remove-Parents']);
}
- $file->save();
+ if ($filename != $file->getProperty('name')) {
+ $file->setProperty('name', $filename);
+ }
+
+ DB::commit();
} elseif ($media == 'resumable' || $media == 'content') {
$params = [];
@@ -659,6 +587,96 @@
return response()->json($response);
}
+ /**
+ * Create a new collection.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ protected function createCollection(Request $request)
+ {
+ // Validate file name input
+ $v = Validator::make($request->all(), [
+ 'name' => ['required', new FileName()],
+ 'deviceId' => ['max:255'],
+ 'collectionType' => ['max:255'],
+ ]);
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
+ $parents = $this->getInputParents($request);
+ if ($errorResponse = $this->validateParents($parents)) {
+ return $errorResponse;
+ }
+
+ $properties = [
+ 'name' => $request->input('name'),
+ 'deviceId' => $request->input('deviceId'),
+ 'collectionType' => $request->input('collectionType'),
+ ];
+
+ foreach ($request->all() as $key => $value) {
+ if (str_starts_with($key, 'property-')) {
+ $propertyKey = substr($key, 9);
+
+ if ($errorResponse = $this->validatePropertyName($propertyKey)) {
+ return $errorResponse;
+ }
+
+ $properties[$propertyKey] = $value;
+ }
+ }
+
+ DB::beginTransaction();
+
+ $item = $this->deduplicateOrCreate($request, Item::TYPE_COLLECTION);
+ $item->setProperties($properties);
+
+ if (!empty($parents)) {
+ $item->parents()->sync($parents);
+ }
+
+ DB::commit();
+
+ return response()->json([
+ 'status' => 'success',
+ 'id' => $item->id,
+ 'message' => self::trans('app.collection-create-success'),
+ ]);
+ }
+
+ /**
+ * Find or create an item, using deduplicate parameters
+ */
+ protected function deduplicateOrCreate(Request $request, $type): Item
+ {
+ $user = $this->guard()->user();
+ $item = null;
+
+ if ($request->has('deduplicate-property')) {
+ // query for item by deduplicate-value
+ $item = $user->fsItems()->select('fs_items.*')
+ ->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)
+ ->whereLike('fs_properties.value', $request->input('deduplicate-value'))
+ ->first();
+
+ // FIXME: Should we throw an error if there's more than one item?
+ }
+
+ if (!$item) {
+ $item = $user->fsItems()->create(['type' => $type]);
+ }
+
+ return $item;
+ }
+
/**
* Convert Permission to an array for the API response.
*
@@ -778,4 +796,59 @@
return $result;
}
+
+ /**
+ * Validate parents list
+ */
+ protected function validateParents($parents)
+ {
+ $user = $this->guard()->user();
+ if (!empty($parents) && count($parents) != $user->fsItems()->whereIn('id', $parents)->count()) {
+ $error = self::trans('validation.fsparentunknown');
+ return response()->json(['status' => 'error', 'errors' => [$error]], 422);
+ }
+
+ return null;
+ }
+
+ /**
+ * Collect collection Ids from input
+ */
+ protected function getInputParents(Request $request): array
+ {
+ $parents = [];
+
+ if ($parentHeader = $request->headers->get('X-Kolab-Parents')) {
+ $parents = explode(',', $parentHeader);
+ }
+
+ if ($parent = $request->input('parent')) {
+ $parents = array_merge($parents, [$parent]);
+ }
+
+ return array_values(array_unique($parents));
+ }
+
+ /**
+ * Validate property name
+ */
+ protected function validatePropertyName(string $name)
+ {
+ if (strlen($name) > 191) {
+ $error = self::trans('validation.max.string', ['attribute' => $name, 'max' => 191]);
+ return response()->json(['status' => 'error', 'errors' => [$error]], 422);
+ }
+
+ if (preg_match('/^(name)$/i', $name)) {
+ $error = self::trans('validation.prohibited', ['attribute' => $name]);
+ return response()->json(['status' => 'error', 'errors' => [$error]], 422);
+ }
+
+ if (!preg_match('/^[a-zA-Z0-9_-]+$/', $name)) {
+ $error = self::trans('validation.regex_format', ['attribute' => $name, 'format' => 'a-zA-Z0-9_-']);
+ return response()->json(['status' => 'error', 'errors' => [$error]], 422);
+ }
+
+ return null;
+ }
}
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
@@ -156,6 +156,7 @@
'file' => [
'create' => "Create file",
'delete' => "Delete file",
+ 'drop' => "Click or drop file(s) here",
'list-empty' => "There are no files in this account.",
'mimetype' => "Mimetype",
'mtime' => "Modified",
diff --git a/src/resources/lang/en/validation.php b/src/resources/lang/en/validation.php
--- a/src/resources/lang/en/validation.php
+++ b/src/resources/lang/en/validation.php
@@ -160,6 +160,7 @@
'file-name-exists' => 'The file name already exists.',
'file-name-invalid' => 'The file name is invalid.',
'file-name-toolong' => 'The file name is too long.',
+ 'fsparentunknown' => 'Specified parent does not exist.',
'geolockinerror' => 'The request location is not allowed.',
'ipolicy-invalid' => 'The specified invitation policy is invalid.',
'invalid-config-parameter' => 'The requested configuration parameter is not supported.',
diff --git a/src/resources/themes/forms.scss b/src/resources/themes/forms.scss
--- a/src/resources/themes/forms.scss
+++ b/src/resources/themes/forms.scss
@@ -171,17 +171,16 @@
}
.file-drop-area {
- display: inline;
- background: $menu-bg-color;
color: grey;
font-size: 0.9rem;
font-weight: normal;
line-height: 2;
- border: 1px solid #eee;
+ border: 1px dashed #bbb;
border-radius: 0.5em;
padding: 0.5em;
cursor: pointer;
position: relative;
+ margin-top: -0.5rem;
input {
position: absolute;
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,21 +1,7 @@
<template>
<div class="container">
<div class="card" id="file-info">
- <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-body">
<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>
@@ -107,22 +93,20 @@
this.fileId = this.$route.params.file
this.collectionId = this.$route.query.parent
- if (this.fileId != 'newCollection') {
- axios.get('/api/v4/fs/' + this.fileId, { loader: true })
- .then(response => {
- this.file = response.data
+ axios.get('/api/v4/fs/' + this.fileId, { loader: true })
+ .then(response => {
+ this.file = response.data
- if (this.file.isOwner) {
- axios.get('api/v4/fs/' + 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/fs/' + this.fileId + '/permissions')
+ .then(response => {
+ if (response.data.list) {
+ this.shares = response.data.list
+ }
+ })
+ }
+ })
+ .catch(this.$root.errorHandler)
},
methods: {
copyLink(link) {
@@ -163,19 +147,6 @@
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/fs', 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,27 +2,17 @@
<div class="container">
<div class="card" id="files">
<div class="card-body">
- <div class="card-title" v-if="collectionId">
- {{ $t('dashboard.files') + ' - ' + collection.name }}
+ <div class="card-title">
+ {{ $t('dashboard.files') }} <span v-if="collectionId" class="me-1">{{ ' - ' + collection.name }}</span>
<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">
+ <btn v-if="!$root.isDegraded()" class="float-end btn-outline-secondary" icon="folder" @click="createCollection">
{{ $t('collection.create') }}
- </btn-router>
+ </btn>
</div>
<div class="card-text pt-4">
+ <div id="drop-area" class="file-drop-area text-center mb-3">
+ <svg-icon icon="upload"></svg-icon> {{ $t('file.drop') }}
+ </div>
<div class="mb-2 d-flex w-100">
<list-search :placeholder="$t('file.search')" :on-search="searchFiles"></list-search>
</div>
@@ -35,19 +25,14 @@
</thead>
<tbody>
<tr v-for="file in files" :key="file.id" @click="$root.clickRecord">
- <td class="name" v-if="file.type === 'collection'">
- <svg-icon icon="folder"></svg-icon>
- <router-link :to="{ path: '/file/' + file.id }">{{ file.name }}</router-link>
+ <td class="name">
+ <router-link :to="(file.type === 'collection' ? '/files/' : '/file/') + `${file.id}`">
+ <svg-icon :icon="file.type === 'collection' ? 'folder' : 'file'" class="me-1"></svg-icon>
+ {{ 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>
- </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" v-else>
- <btn class="button-download p-0 ms-1" @click="fileDownload(file)" icon="download" :title="$t('btn.download')"></btn>
+ <td class="buttons">
+ <btn v-if="file.type !== 'collection'" 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>
</tr>
@@ -58,12 +43,23 @@
</div>
</div>
</div>
+ <modal-dialog id="collection-dialog" ref="collectionDialog" :title="$t('collection.new')" @click="createCollectionSubmit" :buttons="['submit']">
+ <div>
+ <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="form.name">
+ </div>
+ </div>
+ </div>
+ </modal-dialog>
</div>
</template>
<script>
import FileAPI from '../../js/files.js'
import ListTools from '../Widgets/ListTools'
+ import ModalDialog from '../Widgets/ModalDialog'
import { library } from '@fortawesome/fontawesome-svg-core'
@@ -75,6 +71,9 @@
)
export default {
+ components: {
+ ModalDialog
+ },
mixins: [ ListTools ],
beforeRouteUpdate (to, from, next) {
this.collectionId = this.$route.params.parent
@@ -88,12 +87,15 @@
return {
api: {},
collection: {},
+ collectionId: '',
files: [],
- collectionId: null
+ form: {},
+ uploads: {},
}
},
created() {
- this.collectionId = this.$route.params.parent
+ this.collectionId = this.$route.params.parent || ''
+
if (this.collectionId) {
axios.get('/api/v4/fs/' + this.collectionId, { loader: true })
.then(response => {
@@ -103,9 +105,6 @@
}
},
mounted() {
- this.uploads = {}
-
- this.collectionId = this.$route.params.parent
this.api = new FileAPI({
dropArea: '#drop-area',
eventHandler: this.eventHandler,
@@ -115,6 +114,21 @@
this.loadFiles({ init: true })
},
methods: {
+ createCollection() {
+ this.form = { name: '', type: 'collection', parent: this.collectionId }
+ this.$root.clearFormValidation($('#collection-dialog'))
+ this.$refs.collectionDialog.show()
+ },
+ createCollectionSubmit() {
+ this.$root.clearFormValidation($('#collection-dialog'))
+
+ axios.post('/api/v4/fs', this.form)
+ .then(response => {
+ this.$refs.collectionDialog.hide()
+ this.$toast.success(response.data.message)
+ this.loadFiles({ reset: true })
+ })
+ },
eventHandler(name, params) {
const camelCase = name.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (m, chr) => chr.toUpperCase())
const method = camelCase + 'Handler'
@@ -146,6 +160,7 @@
if (this.collectionId) {
params['parent'] = this.collectionId
}
+
this.listSearch('files', 'api/v4/fs', params)
},
searchFiles(search) {
diff --git a/src/resources/vue/Widgets/BtnRouter.vue b/src/resources/vue/Widgets/BtnRouter.vue
--- a/src/resources/vue/Widgets/BtnRouter.vue
+++ b/src/resources/vue/Widgets/BtnRouter.vue
@@ -14,9 +14,12 @@
},
methods: {
className() {
- let label = this.to.length ? this.to : this.to.name
+ let label = (this.to.length ? this.to : this.to.name)
+ .replace(/\?.*$/, '')
+ .replace('/', '-')
+ .replace(/(^[^a-z]+)|([^a-z]+$)/g, '')
- return ['btn', label.replace('/', '-')]
+ return label
}
}
}
diff --git a/src/tests/Feature/Controller/FsTest.php b/src/tests/Feature/Controller/FsTest.php
--- a/src/tests/Feature/Controller/FsTest.php
+++ b/src/tests/Feature/Controller/FsTest.php
@@ -76,6 +76,25 @@
// TODO: Test acting as another user with permissions
}
+ /**
+ * Test deleting collections (DELETE /api/v4/fs/<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/fs/{$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 file downloads (GET /api/v4/fs/downloads/<id>)
*/
@@ -367,6 +386,44 @@
$this->assertSame(1, $json['count']);
}
+ /**
+ * Test fetching file/folders list (GET /api/v4/fs)
+ */
+ public function testIndexChildren(): 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/fs?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/fs?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/fs?parent={$collection->id}");
+ $response->assertStatus(200);
+ $json = $response->json();
+ $this->assertSame(0, count($response->json()['list']));
+ }
+
/**
* Test fetching file metadata (GET /api/v4/fs/<file-id>)
*/
@@ -451,10 +508,20 @@
$this->assertSame('error', $json['status']);
$this->assertSame(["The file name is invalid."], $json['errors']['name']);
+ $response = $this->sendRawBody($john, 'POST', "api/v4/fs?name=test.txt&parent=unknown", [], '');
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertSame(["Specified parent does not exist."], $json['errors']);
+
+ $parent = $this->getTestCollection($john, 'Parent');
+
// Create a file - the simple method
$body = "test content";
$headers = [];
- $response = $this->sendRawBody($john, 'POST', "api/v4/fs?name=test.txt", $headers, $body);
+ $response = $this->sendRawBody($john, 'POST', "api/v4/fs?name=test.txt&parent={$parent->id}", $headers, $body);
$response->assertStatus(200);
$json = $response->json();
@@ -472,6 +539,159 @@
$this->assertSame($json['size'], (int) $file->getProperty('size'));
$this->assertSame($json['name'], $file->getProperty('name'));
$this->assertSame($body, $this->getTestFileContent($file));
+
+ $this->assertSame(1, $file->parents()->count());
+ $this->assertSame($parent->id, $file->parents()->first()->id);
+
+ // TODO: Test X-Kolab-Parents
+ }
+
+ /**
+ * Test creating collections (POST /api/v4/fs?type=collection)
+ */
+ public function testStoreCollection(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+
+ $params = [
+ 'name' => "MyTestCollection",
+ 'deviceId' => "myDeviceId",
+ ];
+
+ // Invalid parent
+ $response = $this->actingAs($john)->post("api/v4/fs?type=collection", $params + ['parent' => 'unknown']);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertSame(["Specified parent does not exist."], $json['errors']);
+
+ // Valid input
+ $response = $this->actingAs($john)->post("api/v4/fs?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/fs?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/fs?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/fs?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/fs?type=collection", $params);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ }
+
+ /**
+ * Test store item relations (POST /api/v4/fs)
+ */
+ 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/fs?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/fs?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/fs)
+ */
+ 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/fs?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);
}
/**
@@ -610,6 +830,47 @@
// TODO: Test media=resumable
}
+ /**
+ * Test update item relations (PUT /api/v4/fs/$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/fs/{$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/fs/{$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/fs/{$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);
+ }
+
/**
* Create a test file.
*
@@ -744,237 +1005,4 @@
return $this->call($method, $uri, [], $cookies, [], $server, $content);
}
}
-
-
- /**
- * Test creating collections (POST /api/v4/fs?type=collection)
- */
- public function testStoreCollection(): void
- {
- $john = $this->getTestUser('john@kolab.org');
-
- $params = [
- 'name' => "MyTestCollection",
- 'deviceId' => "myDeviceId",
- ];
- $response = $this->actingAs($john)->post("api/v4/fs?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/fs?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/fs?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/fs?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/fs?type=collection", $params);
- $response->assertStatus(200);
-
- $json = $response->json();
- $this->assertSame('success', $json['status']);
- }
-
- /**
- * Test deleting collections (DELETE /api/v4/fs/<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/fs/{$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/fs)
- */
- 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/fs?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/fs?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/fs)
- */
- 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/fs?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/fs/$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/fs/{$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/fs/{$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/fs/{$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/fs?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/fs?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/fs?parent={$collection->id}");
- $response->assertStatus(200);
- $json = $response->json();
- $this->assertSame(0, count($response->json()['list']));
- }
}

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 10, 3:08 PM (9 h, 38 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18856566
Default Alt Text
D4364.1775833730.diff (48 KB)

Event Timeline