Page MenuHomePhorge

D5751.1775203849.diff
No OneTemporary

Authored By
Unknown
Size
18 KB
Referenced Files
None
Subscribers
None

D5751.1775203849.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
@@ -5,9 +5,12 @@
use App\Fs\Item;
use App\Fs\Property;
use App\Http\Controllers\RelationController;
+use App\Http\Resources\FsItemInfoResource;
+use App\Http\Resources\FsItemResource;
use App\Rules\FileName;
use App\Support\Facades\Storage;
use App\Utils;
+use Dedoc\Scramble\Attributes\QueryParameter;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
@@ -21,10 +24,6 @@
protected const READ = 'r';
protected const WRITE = 'w';
- protected const TYPE_COLLECTION = 'collection';
- protected const TYPE_FILE = 'file';
- protected const TYPE_UNKNOWN = 'unknown';
-
/** @var string Resource localization label */
protected $label = 'file';
@@ -264,8 +263,12 @@
}
/**
- * Listing of files (and folders).
+ * Listing of files and folders.
*/
+ #[QueryParameter('search', description: 'Search string', type: 'string')]
+ #[QueryParameter('page', description: 'Page number', type: 'int')]
+ #[QueryParameter('parent', description: 'Parent identifier', type: 'string')]
+ #[QueryParameter('type', description: 'Item type to return (collection or file)', type: 'string')]
public function index(): JsonResponse
{
$search = trim(request()->input('search'));
@@ -275,9 +278,11 @@
$pageSize = 100;
$hasMore = false;
- $user = $this->guard()->user();
-
- $result = $user->fsItems()->select('fs_items.*', 'fs_properties.value as name');
+ $result = $this->guard()->user()->fsItems()
+ ->select('fs_items.*', 'fs_properties.value as name')
+ ->join('fs_properties', 'fs_items.id', '=', 'fs_properties.item_id')
+ ->where('key', 'name')
+ ->whereNot('type', '&', Item::TYPE_INCOMPLETE);
if ($parent) {
$result->join('fs_relations', 'fs_items.id', '=', 'fs_relations.related_id')
@@ -287,13 +292,14 @@
->whereNull('fs_relations.related_id');
}
- // Add properties
- $result->join('fs_properties', 'fs_items.id', '=', 'fs_properties.item_id')
- ->whereNot('type', '&', Item::TYPE_INCOMPLETE)
- ->where('key', 'name');
+ // Additional properties
+ foreach (['size', 'mimetype'] as $key) {
+ $result->selectRaw('(select value from fs_properties where fs_items.id = fs_properties.item_id'
+ . " and fs_properties.key = '{$key}') as {$key}");
+ }
if ($type) {
- if ($type == self::TYPE_COLLECTION) {
+ if ($type == FsItemResource::TYPE_COLLECTION) {
$result->where('type', '&', Item::TYPE_COLLECTION);
} else {
$result->where('type', '&', Item::TYPE_FILE);
@@ -304,7 +310,8 @@
$result->whereLike('fs_properties.value', "%{$search}%");
}
- $result = $result->orderBy('name')
+ // Sort by name, but collections first
+ $result = $result->orderByRaw('case when fs_items.type & ' . Item::TYPE_COLLECTION . ' then 0 else 1 end, name')
->limit($pageSize + 1)
->offset($pageSize * ($page - 1))
->get();
@@ -314,32 +321,24 @@
$hasMore = true;
}
- // Process the result
- // @phpstan-ignore argument.unresolvableType
- $result = $result->map(function ($file) {
- // TODO: This is going to be 100 SELECT queries (with pageSize=100), we should get
- // file properties using the main query
- $result = $this->objectToClient($file);
- $result['name'] = $file->name; // @phpstan-ignore-line
-
- return $result;
- });
-
- $result = [
- 'list' => $result,
+ return response()->json([
+ // List of filesystem items
+ 'list' => FsItemResource::collection($result),
+ // @var int Number of entries in the list
'count' => count($result),
+ // @var bool Indicates that there are more entries available
'hasMore' => $hasMore,
- ];
-
- return response()->json($result);
+ ]);
}
/**
- * Fetch the specific file metadata or content.
+ * Fetch file/folder metadata or content.
*
- * @param string $id the file identifier
+ * @param string $id The file/folder identifier
*/
- public function show($id): JsonResponse|StreamedResponse
+ #[QueryParameter('download', description: 'Request the file content', type: 'bool')]
+ #[QueryParameter('downloadUrl', description: 'Request a unique download URL for the file content', type: 'bool')]
+ public function show($id): FsItemInfoResource|JsonResponse|StreamedResponse
{
$file = $this->inputItem($id, self::READ);
@@ -347,27 +346,21 @@
return $this->errorResponse($file);
}
- $response = $this->objectToClient($file, true);
+ if (request()->input('download')) {
+ // Return the file content
+ return Storage::fileDownload($file);
+ }
+
+ $response = new FsItemInfoResource($file);
if (request()->input('downloadUrl')) {
// 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/fs/downloads/' . $downloadId);
- } elseif (request()->input('download')) {
- // Return the file content
- return Storage::fileDownload($file);
+ $response->downloadUrl = Utils::serviceUrl('api/v4/fs/downloads/' . $downloadId);
}
- $response['mtime'] = $file->updated_at->format('Y-m-d H:i');
-
- // TODO: Handle read-write/full access rights
- $isOwner = $this->guard()->user()->id == $file->user_id;
- $response['canUpdate'] = $isOwner;
- $response['canDelete'] = $isOwner;
- $response['isOwner'] = $isOwner;
-
- return response()->json($response);
+ return $response;
}
/**
@@ -380,7 +373,7 @@
public function store(Request $request)
{
$type = $request->input('type');
- if ($type == self::TYPE_COLLECTION) {
+ if ($type == FsItemResource::TYPE_COLLECTION) {
return $this->createCollection($request);
}
@@ -771,11 +764,11 @@
{
$result = ['id' => $object->id];
if ($object->isCollection()) {
- $result['type'] = self::TYPE_COLLECTION;
+ $result['type'] = FsItemResource::TYPE_COLLECTION;
} elseif ($object->isFile()) {
- $result['type'] = self::TYPE_FILE;
+ $result['type'] = FsItemResource::TYPE_FILE;
} else {
- $result['type'] = self::TYPE_UNKNOWN;
+ $result['type'] = FsItemResource::TYPE_UNKNOWN;
}
if ($full) {
diff --git a/src/app/Http/Resources/FsItemInfoResource.php b/src/app/Http/Resources/FsItemInfoResource.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Resources/FsItemInfoResource.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace App\Http\Resources;
+
+use App\Fs\Item;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+
+/**
+ * Filesystem item information response
+ *
+ * @mixin Item
+ */
+class FsItemInfoResource extends FsItemResource
+{
+ public ?string $downloadUrl = null;
+
+ /**
+ * Transform the resource into an array.
+ */
+ public function toArray(Request $request): array
+ {
+ // TODO: Handle read-write/full access rights
+ $isOwner = Auth::guard()->user()->id == $this->resource->user_id;
+
+ $parent = $this->resource->parents()->first();
+
+ return [
+ $this->merge(parent::toArray($request)),
+
+ 'canUpdate' => $isOwner,
+ 'canDelete' => $isOwner,
+ 'isOwner' => $isOwner,
+
+ // @var string Unauthenticated doawnload location for the file content
+ 'downloadUrl' => $this->when(!empty($this->downloadUrl), $this->downloadUrl),
+
+ // @var ?string Parent folder identifier
+ 'parentId' => $parent?->id,
+ ];
+ }
+}
diff --git a/src/app/Http/Resources/FsItemResource.php b/src/app/Http/Resources/FsItemResource.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Resources/FsItemResource.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace App\Http\Resources;
+
+use App\Fs\Item;
+use Illuminate\Http\Request;
+
+/**
+ * Filesystem item response
+ *
+ * @mixin Item
+ */
+class FsItemResource extends ApiResource
+{
+ public const TYPE_COLLECTION = 'collection';
+ public const TYPE_FILE = 'file';
+ public const TYPE_UNKNOWN = 'unknown';
+
+ /**
+ * Transform the resource into an array.
+ */
+ public function toArray(Request $request): array
+ {
+ $is_file = false;
+ if ($this->resource->isCollection()) {
+ $type = self::TYPE_COLLECTION;
+ } elseif ($this->resource->isFile()) {
+ $is_file = true;
+ $type = self::TYPE_FILE;
+ } else {
+ $type = self::TYPE_UNKNOWN;
+ }
+
+ $keys = ['name', 'size', 'mimetype'];
+ $props = [];
+ if (!isset($this->resource->name) || ($is_file && (!isset($this->resource->size) || !isset($this->resource->mimetype)))) {
+ $props = array_filter($this->resource->getProperties($keys));
+ } else {
+ foreach ($keys as $key) {
+ $props[$key] = $this->resource->{$key} ?? null;
+ }
+ }
+
+ return [
+ // @var string Item identifier
+ 'id' => $this->resource->id,
+ // @var string Item type (collection, file, unknown)
+ 'type' => $type,
+ // @var string<date-time> Creation time
+ 'created_at' => $this->resource->created_at->toDateTimeString(),
+ // @var string<date-time> Last update time
+ 'updated_at' => $this->resource->updated_at->toDateTimeString(),
+
+ // @var string Item name
+ 'name' => $props['name'] ?? '',
+ // @var int File size
+ 'size' => $this->when($is_file, (int) ($props['size'] ?? 0)),
+ // @var string File content type
+ 'mimetype' => $this->when($is_file && isset($props['mimetype']), $props['mimetype'] ?? ''),
+ ];
+ }
+}
diff --git a/src/resources/themes/app.scss b/src/resources/themes/app.scss
--- a/src/resources/themes/app.scss
+++ b/src/resources/themes/app.scss
@@ -200,12 +200,12 @@
overflow: hidden;
text-overflow: ellipsis;
}
-/*
+
td.size,
th.size {
- width: 80px;
+ width: 5em;
}
-
+/*
td.mtime,
th.mtime {
width: 140px;
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
@@ -25,7 +25,7 @@
<div class="row plaintext mb-3">
<label for="mtime" class="col-sm-4 col-form-label">{{ $t('file.mtime') }}</label>
<div class="col-sm-8">
- <span class="form-control-plaintext" id="mtime">{{ file.mtime }}</span>
+ <span class="form-control-plaintext" id="mtime">{{ file.created_at }}</span>
</div>
</div>
<btn class="btn-primary" icon="download" @click="fileDownload">{{ $t('btn.download') }}</btn>
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
@@ -20,24 +20,35 @@
<thead>
<tr>
<th scope="col" class="name">{{ $t('form.name') }}</th>
+ <th scope="col" class="size">{{ $t('form.size') }}</th>
<th scope="col" class="buttons"></th>
</tr>
</thead>
<tbody>
+ <tr>
+ <td v-if="collectionId" colspan="3" class="name">
+ <router-link :to="'/files' + (collection.parentId ? `/{$collection.parentId}` : '')">
+ <svg-icon icon="folder" class="me-1"></svg-icon> ..
+ </router-link>
+ </td>
+ </tr>
<tr v-for="file in files" :key="file.id" @click="$root.clickRecord">
<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>
+ <svg-icon :icon="file.type === 'collection' ? 'folder' : ['far','file']" class="me-1" style="width:1em"></svg-icon>
{{ file.name }}
</router-link>
</td>
+ <td class="size">
+ <span>{{ fileSize(file) }}</span>
+ </td>
<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>
</tbody>
- <list-foot :colspan="2" :text="$t('file.list-empty')"></list-foot>
+ <list-foot :colspan="3" :text="$t('file.list-empty')"></list-foot>
</table>
<list-more v-if="hasMore" :on-click="loadFiles"></list-more>
</div>
@@ -64,7 +75,7 @@
import { library } from '@fortawesome/fontawesome-svg-core'
library.add(
- require('@fortawesome/free-solid-svg-icons/faFile').definition,
+ require('@fortawesome/free-regular-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,
@@ -156,6 +167,13 @@
// require authentication) and then use it to download the file.
this.api.fileDownload(file.id)
},
+ fileSize(file) {
+ if (file.type != 'file') {
+ return '';
+ }
+
+ return this.api.sizeText(file.size)
+ },
loadFiles(params) {
if (this.collectionId) {
params['parent'] = this.collectionId
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
@@ -313,6 +313,7 @@
// Create some files 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]);
+ $folder = $this->getTestCollection($user, 'VVV');
$response = $this->actingAs($user)->get("api/v4/fs");
$response->assertStatus(200);
@@ -320,13 +321,21 @@
$json = $response->json();
$this->assertCount(3, $json);
- $this->assertSame(2, $json['count']);
+ $this->assertSame(3, $json['count']);
$this->assertFalse($json['hasMore']);
- $this->assertCount(2, $json['list']);
- $this->assertSame('test1.txt', $json['list'][0]['name']);
- $this->assertSame($file1->id, $json['list'][0]['id']);
- $this->assertSame('test2.gif', $json['list'][1]['name']);
- $this->assertSame($file2->id, $json['list'][1]['id']);
+ $this->assertCount(3, $json['list']);
+ $this->assertSame('VVV', $json['list'][0]['name']);
+ $this->assertSame($folder->id, $json['list'][0]['id']);
+ $this->assertFalse(in_array('size', $json['list'][0]));
+ $this->assertFalse(in_array('mimetype', $json['list'][0]));
+ $this->assertSame('test1.txt', $json['list'][1]['name']);
+ $this->assertSame(12345, $json['list'][1]['size']);
+ $this->assertSame($file1->id, $json['list'][1]['id']);
+ $this->assertSame('text/plain', $json['list'][1]['mimetype']);
+ $this->assertSame('test2.gif', $json['list'][2]['name']);
+ $this->assertSame($file2->id, $json['list'][2]['id']);
+ $this->assertSame(10000, $json['list'][2]['size']);
+ $this->assertSame('image/gif', $json['list'][2]['mimetype']);
// Searching
$response = $this->actingAs($user)->get("api/v4/fs?search=t2");
@@ -353,7 +362,7 @@
$json = $response->json();
$this->assertCount(3, $json);
- $this->assertSame(1, $json['count']);
+ $this->assertSame(2, $json['count']);
}
/**
@@ -427,9 +436,22 @@
$this->assertSame($file->getProperty('mimetype'), $json['mimetype']);
$this->assertSame((int) $file->getProperty('size'), $json['size']);
$this->assertSame($file->getProperty('name'), $json['name']);
+ $this->assertSame('file', $json['type']);
$this->assertTrue($json['isOwner']);
$this->assertTrue($json['canUpdate']);
$this->assertTrue($json['canDelete']);
+ $this->assertNull($json['parentId']);
+
+ $folder = $this->getTestCollection($john, 'VVV');
+ $folder->children()->attach($file);
+
+ // Get file metadata (file in a folder)
+ $response = $this->actingAs($john)->get("api/v4/fs/{$file->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame($folder->id, $json['parentId']);
// Get file content
$response = $this->actingAs($john)->get("api/v4/fs/{$file->id}?download=1");

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 8:10 AM (19 h, 43 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18823163
Default Alt Text
D5751.1775203849.diff (18 KB)

Event Timeline