Skip to content

Commit 9030a09

Browse files
Carl SchwanCarl Schwan
authored andcommitted
perf(s3): Expose preview pre-signed urls in WebDAV
Allows client to directly fetch then. Signed-off-by: Carl Schwan <[email protected]>
1 parent cc23025 commit 9030a09

File tree

10 files changed

+110
-29
lines changed

10 files changed

+110
-29
lines changed

apps/dav/lib/Connector/Sabre/File.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -539,10 +539,6 @@ public function getContentType() {
539539
return Server::get(IMimeTypeDetector::class)->getSecureMimeType($mimeType);
540540
}
541541

542-
/**
543-
* @throws NotFoundException
544-
* @throws NotPermittedException
545-
*/
546542
public function getDirectDownload(): array|false {
547543
if (Server::get(IAppManager::class)->isEnabledForUser('encryption')) {
548544
return false;
@@ -552,7 +548,6 @@ public function getDirectDownload(): array|false {
552548
if (!$storage) {
553549
return false;
554550
}
555-
556551
if (!($node->getPermissions() & Constants::PERMISSION_READ)) {
557552
return false;
558553
}

apps/dav/lib/Connector/Sabre/FilesPlugin.php

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
use OC\AppFramework\Http\Request;
1111
use OC\FilesMetadata\Model\FilesMetadata;
12+
use OC\Preview\Db\Preview;
13+
use OC\Preview\PreviewService;
14+
use OC\Preview\Storage\IPreviewStorage;
1215
use OC\User\NoUserException;
1316
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
1417
use OCA\Files_Sharing\External\Mount as SharingExternalMount;
@@ -30,6 +33,7 @@
3033
use OCP\L10N\IFactory;
3134
use Sabre\DAV\Exception\Forbidden;
3235
use Sabre\DAV\Exception\NotFound;
36+
use Sabre\DAV\ICollection;
3337
use Sabre\DAV\IFile;
3438
use Sabre\DAV\PropFind;
3539
use Sabre\DAV\PropPatch;
@@ -61,6 +65,7 @@ class FilesPlugin extends ServerPlugin {
6165
public const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums';
6266
public const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint';
6367
public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
68+
public const PREVIEW_METADATA_PROPERTYNAME = '{http://nextcloud.org/ns}preview-metadata';
6469
public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type';
6570
public const MOUNT_ROOT_PROPERTYNAME = '{http://nextcloud.org/ns}is-mount-root';
6671
public const IS_FEDERATED_PROPERTYNAME = '{http://nextcloud.org/ns}is-federated';
@@ -77,6 +82,8 @@ class FilesPlugin extends ServerPlugin {
7782
/** Reference to main server object */
7883
private ?Server $server = null;
7984

85+
private array $previewMetadataCache = [];
86+
8087
/**
8188
* @param Tree $tree
8289
* @param IConfig $config
@@ -95,6 +102,8 @@ public function __construct(
95102
private IUserSession $userSession,
96103
private IFilenameValidator $validator,
97104
private IAccountManager $accountManager,
105+
private PreviewService $previewService,
106+
private IPreviewStorage $previewStorage,
98107
private bool $isPublic = false,
99108
private bool $downloadAttachment = true,
100109
) {
@@ -127,6 +136,7 @@ public function initialize(Server $server) {
127136
$server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME;
128137
$server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME;
129138
$server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME;
139+
$server->protectedProperties[] = self::PREVIEW_METADATA_PROPERTYNAME;
130140
$server->protectedProperties[] = self::MOUNT_TYPE_PROPERTYNAME;
131141
$server->protectedProperties[] = self::IS_FEDERATED_PROPERTYNAME;
132142
$server->protectedProperties[] = self::SHARE_NOTE;
@@ -136,6 +146,7 @@ public function initialize(Server $server) {
136146
$server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties);
137147

138148
$this->server = $server;
149+
$this->server->on('preloadCollection', $this->preloadCollection(...));
139150
$this->server->on('propFind', [$this, 'handleGetProperties']);
140151
$this->server->on('propPatch', [$this, 'handleUpdateProperties']);
141152
$this->server->on('afterBind', [$this, 'sendFileIdHeader']);
@@ -152,6 +163,23 @@ public function initialize(Server $server) {
152163
$this->server->on('beforeCopy', [$this, 'checkCopy']);
153164
}
154165

166+
private function preloadCollection(PropFind $propFind, ICollection $collection): void {
167+
if (!($collection instanceof Directory)) {
168+
return;
169+
}
170+
171+
$requestProperties = $propFind->getRequestedProperties();
172+
if (in_array(self::PREVIEW_METADATA_PROPERTYNAME, $requestProperties, true)) {
173+
$keys = array_map(static fn (Node $node) => $node->getInternalFileId(), $collection->getChildren());
174+
$missingKeys = array_diff($keys, array_keys($this->previewMetadataCache));
175+
if (count($missingKeys) > 0) {
176+
foreach ($this->previewService->getAvailablePreviews($missingKeys) as $fileId => $previews) {
177+
$this->previewMetadataCache[$fileId] = $previews;
178+
}
179+
}
180+
}
181+
}
182+
155183
/**
156184
* Plugin that checks if a copy can actually be performed.
157185
*
@@ -474,29 +502,34 @@ public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node)
474502

475503
if ($node instanceof File) {
476504
$requestProperties = $propFind->getRequestedProperties();
477-
478505
if (in_array(self::DOWNLOADURL_PROPERTYNAME, $requestProperties, true)
479506
|| in_array(self::DOWNLOADURL_EXPIRATION_PROPERTYNAME, $requestProperties, true)) {
480507
try {
481508
$directDownloadUrl = $node->getDirectDownload();
509+
if ($directDownloadUrl && isset($directDownloadUrl['url'])) {
510+
$propFind->handle(self::DOWNLOADURL_PROPERTYNAME, $directDownloadUrl['url']);
511+
$propFind->handle(self::DOWNLOADURL_EXPIRATION_PROPERTYNAME, $directDownloadUrl['expiration']);
512+
}
482513
} catch (StorageNotAvailableException|ForbiddenException) {
483-
$directDownloadUrl = null;
514+
$propFind->handle(self::DOWNLOADURL_PROPERTYNAME, false);
515+
$propFind->handle(self::DOWNLOADURL_EXPIRATION_PROPERTYNAME, false);
484516
}
517+
}
485518

486-
$propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node, $directDownloadUrl) {
487-
if ($directDownloadUrl && isset($directDownloadUrl['url'])) {
488-
return $directDownloadUrl['url'];
519+
$propFind->handle(self::PREVIEW_METADATA_PROPERTYNAME, function () use ($node) {
520+
try {
521+
if (isset($this->previewMetadataCache[$node->getInternalFileId()])) {
522+
$previews = $this->previewMetadataCache[$node->getInternalFileId()];
523+
} else {
524+
[$node->getInternalFileId() => $previews] = $this->previewService->getAvailablePreviews([$node->getInternalFileId()]);
489525
}
490-
return false;
491-
});
492526

493-
$propFind->handle(self::DOWNLOADURL_EXPIRATION_PROPERTYNAME, function () use ($node, $directDownloadUrl) {
494-
if ($directDownloadUrl && isset($directDownloadUrl['expiration'])) {
495-
return $directDownloadUrl['expiration'];
496-
}
497-
return false;
498-
});
499-
}
527+
$data = array_map(fn (Preview $preview) => Preview::getMetadata($preview, $this->previewStorage), $previews);
528+
return json_encode($data, JSON_THROW_ON_ERROR);
529+
} catch (\Exception $e) {
530+
}
531+
return false;
532+
});
500533

501534
$propFind->handle(self::CHECKSUMS_PROPERTYNAME, function () use ($node) {
502535
$checksum = $node->getChecksum();

apps/dav/lib/Connector/Sabre/ServerFactory.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
use OC\Files\View;
1111
use OC\KnownUser\KnownUserService;
12+
use OC\Preview\PreviewService;
13+
use OC\Preview\Storage\StorageFactory;
1214
use OCA\DAV\AppInfo\PluginManager;
1315
use OCA\DAV\CalDAV\DefaultCalendarValidator;
1416
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
@@ -162,6 +164,8 @@ public function createServer(
162164
$this->userSession,
163165
\OCP\Server::get(IFilenameValidator::class),
164166
\OCP\Server::get(IAccountManager::class),
167+
\OCP\Server::get(PreviewService::class),
168+
\OCP\Server::get(StorageFactory::class),
165169
$isPublicShare,
166170
!$debugEnabled
167171
)

apps/dav/lib/Server.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
namespace OCA\DAV;
99

1010
use OC\Files\Filesystem;
11+
use OC\Preview\PreviewService;
12+
use OC\Preview\Storage\StorageFactory;
1113
use OCA\DAV\AppInfo\PluginManager;
1214
use OCA\DAV\BulkUpload\BulkUploadPlugin;
1315
use OCA\DAV\CalDAV\BirthdayCalendar\EnablePlugin;
@@ -300,6 +302,8 @@ public function __construct(
300302
\OCP\Server::get(IUserSession::class),
301303
\OCP\Server::get(IFilenameValidator::class),
302304
\OCP\Server::get(IAccountManager::class),
305+
\OCP\Server::get(PreviewService::class),
306+
\OCP\Server::get(StorageFactory::class),
303307
false,
304308
$config->getSystemValueBool('debug', false) === false,
305309
)

lib/private/Files/ObjectStore/S3ConnectionTrait.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ trait S3ConnectionTrait {
3434
private ?ICache $existingBucketsCache = null;
3535
private bool $usePresignedUrl = false;
3636

37+
private bool $usePresignedUrl = false;
38+
3739
protected function parseParams($params) {
3840
if (empty($params['bucket'])) {
3941
throw new \Exception('Bucket has to be configured.');

lib/private/Preview/Db/Preview.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
namespace OC\Preview\Db;
1212

13+
use OC\Preview\Storage\IPreviewStorage;
1314
use OCP\AppFramework\Db\Entity;
1415
use OCP\DB\Types;
1516
use OCP\Files\IMimeTypeDetector;
@@ -179,4 +180,15 @@ public function getSourceMimeType(): string {
179180
public function setSourceMimeType(string $mimeType): void {
180181
$this->sourceMimetype = $mimeType;
181182
}
183+
184+
public static function getMetadata(Preview $preview, IPreviewStorage $storage): mixed {
185+
$data = $storage->getDirectDownload($preview);
186+
return [
187+
'width' => $preview->width,
188+
'height' => $preview->height,
189+
'cropped' => $preview->cropped,
190+
'preview_url' => $data === false ? null : $data['url'],
191+
'preview_url_expiration' => $data === false ? null : $data['expiration'],
192+
];
193+
}
182194
}

lib/private/Preview/Storage/IPreviewStorage.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,9 @@ public function migratePreview(Preview $preview, SimpleFile $file): void;
5050
* @throws NotFoundException
5151
*/
5252
public function scan(): int;
53+
54+
/**
55+
* See IStorage::getDirectDownload
56+
*/
57+
public function getDirectDownload(Preview $preview): array|false;
5358
}

lib/private/Preview/Storage/LocalPreviewStorage.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,4 +241,9 @@ private function deleteParentsFromFileCache(string $dirname): void {
241241
$this->connection->commit();
242242
}
243243
}
244+
245+
#[Override]
246+
public function getDirectDownload(Preview $preview): array|false {
247+
return false;
248+
}
244249
}

lib/private/Preview/Storage/ObjectStorePreviewStorage.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,20 @@ public function getUrn(Preview $preview, array $config): string {
183183
public function scan(): int {
184184
return 0;
185185
}
186+
187+
#[Override]
188+
public function getDirectDownload(Preview $preview): array|false {
189+
[
190+
'urn' => $urn,
191+
'store' => $store,
192+
] = $this->getObjectStoreInfoForExistingPreview($preview);
193+
194+
try {
195+
$expiration = new \DateTimeImmutable('+60 minutes');
196+
$url = $store->preSignedUrl($urn, $expiration);
197+
return $url ? ['url' => $url, 'expiration' => $expiration->getTimestamp()] : false;
198+
} catch (\Exception $exception) {
199+
throw new NotPermittedException('Unable to read preview from object store with urn:' . $urn, previous: $exception);
200+
}
201+
}
186202
}

lib/private/Preview/Storage/StorageFactory.php

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,21 @@ public function deletePreview(Preview $preview): void {
3939
$this->getBackend()->deletePreview($preview);
4040
}
4141

42+
#[Override]
43+
public function migratePreview(Preview $preview, SimpleFile $file): void {
44+
$this->getBackend()->migratePreview($preview, $file);
45+
}
46+
47+
#[Override]
48+
public function scan(): int {
49+
return $this->getBackend()->scan();
50+
}
51+
52+
#[Override]
53+
public function getDirectDownload(Preview $preview): array|false {
54+
return $this->getBackend()->getDirectDownload($preview);
55+
}
56+
4257
private function getBackend(): IPreviewStorage {
4358
if ($this->backend) {
4459
return $this->backend;
@@ -52,14 +67,4 @@ private function getBackend(): IPreviewStorage {
5267

5368
return $this->backend;
5469
}
55-
56-
#[Override]
57-
public function migratePreview(Preview $preview, SimpleFile $file): void {
58-
$this->getBackend()->migratePreview($preview, $file);
59-
}
60-
61-
#[Override]
62-
public function scan(): int {
63-
return $this->getBackend()->scan();
64-
}
6570
}

0 commit comments

Comments
 (0)