Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F118528768
D4364.1775872723.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
48 KB
Referenced Files
None
Subscribers
None
D4364.1775872723.diff
View Options
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 = [];
@@ -660,6 +588,96 @@
}
/**
+ * 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.
*
* @param string $id Permission identifier
@@ -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
@@ -77,6 +77,25 @@
}
/**
+ * 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>)
*/
public function testDownload(): void
@@ -368,6 +387,44 @@
}
/**
+ * 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>)
*/
public function testShow(): void
@@ -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);
}
/**
@@ -611,6 +831,47 @@
}
/**
+ * 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.
*
* @param \App\User $user File owner
@@ -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
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 11, 1:58 AM (1 h, 11 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18854984
Default Alt Text
D4364.1775872723.diff (48 KB)
Attached To
Mode
D4364: Refactor Files UI, plus code improvements
Attached
Detach File
Event Timeline