Skip to content

Commit cbaf6e7

Browse files
Merge pull request #53634 from invario/preview-direct-download
feat(previews): allow ffmpeg to connect direct for AWS S3 buckets
2 parents 3580680 + 98192fc commit cbaf6e7

File tree

4 files changed

+137
-62
lines changed

4 files changed

+137
-62
lines changed

apps/files_external/lib/Lib/Backend/AmazonS3.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public function __construct(IL10N $l, AccessKey $legacyAuth) {
2828
->setFlag(DefinitionParameter::FLAG_OPTIONAL),
2929
(new DefinitionParameter('port', $l->t('Port')))
3030
->setFlag(DefinitionParameter::FLAG_OPTIONAL),
31+
(new DefinitionParameter('proxy', $l->t('Proxy')))
32+
->setFlag(DefinitionParameter::FLAG_OPTIONAL),
3133
(new DefinitionParameter('region', $l->t('Region')))
3234
->setFlag(DefinitionParameter::FLAG_OPTIONAL),
3335
(new DefinitionParameter('storageClass', $l->t('Storage Class')))
@@ -42,6 +44,9 @@ public function __construct(IL10N $l, AccessKey $legacyAuth) {
4244
(new DefinitionParameter('useMultipartCopy', $l->t('Enable multipart copy')))
4345
->setType(DefinitionParameter::VALUE_BOOLEAN)
4446
->setDefaultValue(true),
47+
(new DefinitionParameter('use_presigned_url', $l->t('Use presigned S3 url')))
48+
->setType(DefinitionParameter::VALUE_BOOLEAN)
49+
->setDefaultValue(false),
4550
(new DefinitionParameter('sse_c_key', $l->t('SSE-C encryption key')))
4651
->setType(DefinitionParameter::VALUE_PASSWORD)
4752
->setFlag(DefinitionParameter::FLAG_OPTIONAL),

apps/files_external/lib/Lib/Storage/AmazonS3.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use OCP\ICacheFactory;
2424
use OCP\ITempManager;
2525
use OCP\Server;
26+
use Override;
2627
use Psr\Log\LoggerInterface;
2728

2829
class AmazonS3 extends Common {
@@ -760,4 +761,44 @@ public function writeStream(string $path, $stream, ?int $size = null): int {
760761

761762
return $size;
762763
}
764+
765+
#[Override]
766+
public function getDirectDownload(string $path): array|false {
767+
if (!$this->isUsePresignedUrl()) {
768+
return false;
769+
}
770+
771+
$command = $this->getConnection()->getCommand('GetObject', [
772+
'Bucket' => $this->bucket,
773+
'Key' => $path,
774+
]);
775+
$expiration = new \DateTimeImmutable('+60 minutes');
776+
777+
try {
778+
// generate a presigned URL that expires after $expiration time
779+
$presignedUrl = (string)$this->getConnection()->createPresignedRequest($command, $expiration, [
780+
'signPayload' => true,
781+
])->getUri();
782+
} catch (S3Exception $exception) {
783+
$this->logger->error($exception->getMessage(), [
784+
'app' => 'files_external',
785+
'exception' => $exception,
786+
]);
787+
return false;
788+
}
789+
return [
790+
'url' => $presignedUrl,
791+
'expiration' => $expiration->getTimestamp(),
792+
];
793+
}
794+
795+
#[Override]
796+
public function getDirectDownloadById(string $fileId): array|false {
797+
if (!$this->isUsePresignedUrl()) {
798+
return false;
799+
}
800+
801+
$entry = $this->getCache()->get((int)$fileId);
802+
return $this->getDirectDownload($entry->getPath());
803+
}
763804
}

lib/private/Files/ObjectStore/S3ObjectTrait.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -298,15 +298,15 @@ public function copyObject($from, $to, array $options = []) {
298298
}
299299

300300
public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string {
301+
if (!$this->isUsePresignedUrl()) {
302+
return null;
303+
}
304+
301305
$command = $this->getConnection()->getCommand('GetObject', [
302306
'Bucket' => $this->getBucket(),
303307
'Key' => $urn,
304308
]);
305309

306-
if (!$this->isUsePresignedUrl()) {
307-
return null;
308-
}
309-
310310
try {
311311
return (string)$this->getConnection()->createPresignedRequest($command, $expiration, [
312312
'signPayload' => true,

lib/private/Preview/Movie.php

Lines changed: 87 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,23 @@ public function isAvailable(FileInfo $file): bool {
4242
return is_string($this->binary);
4343
}
4444

45+
private function connectDirect(File $file): string|false {
46+
if ($file->isEncrypted()) {
47+
return false;
48+
}
49+
50+
// Checks for availability to access the video file directly via HTTP/HTTPS.
51+
// Returns a string containing URL if available. Only implemented and tested
52+
// with Amazon S3 currently. In all other cases, return false. ffmpeg
53+
// supports other protocols so this function may expand in the future.
54+
$gddValues = $file->getStorage()->getDirectDownloadById((string)$file->getId());
55+
56+
if (is_array($gddValues) && array_key_exists('url', $gddValues)) {
57+
return str_starts_with($gddValues['url'], 'http') ? $gddValues['url'] : false;
58+
}
59+
return false;
60+
}
61+
4562
/**
4663
* {@inheritDoc}
4764
*/
@@ -54,74 +71,87 @@ public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
5471

5572
$result = null;
5673

74+
$connectDirect = $this->connectDirect($file);
75+
5776
// Timestamps to make attempts to generate a still
5877
$timeAttempts = [5, 1, 0];
5978

60-
// By default, download $sizeAttempts from the file along with
61-
// the 'moov' atom.
62-
// Example bitrates in the higher range:
63-
// 4K HDR H265 60 FPS = 75 Mbps = 9 MB per second needed for a still
64-
// 1080p H265 30 FPS = 10 Mbps = 1.25 MB per second needed for a still
65-
// 1080p H264 30 FPS = 16 Mbps = 2 MB per second needed for a still
66-
$sizeAttempts = [1024 * 1024 * 10];
67-
68-
if ($this->useTempFile($file)) {
69-
if ($file->getStorage()->isLocal()) {
70-
// Temp file required but file is local, so retrieve $sizeAttempt bytes first,
71-
// and if it doesn't work, retrieve the entire file.
72-
$sizeAttempts[] = null;
79+
// If HTTP/HTTPS direct connect is not available or if the file is encrypted,
80+
// process normally
81+
if ($connectDirect === false) {
82+
// By default, download $sizeAttempts from the file along with
83+
// the 'moov' atom.
84+
// Example bitrates in the higher range:
85+
// 4K HDR H265 60 FPS = 75 Mbps = 9 MB per second needed for a still
86+
// 1080p H265 30 FPS = 10 Mbps = 1.25 MB per second needed for a still
87+
// 1080p H264 30 FPS = 16 Mbps = 2 MB per second needed for a still
88+
$sizeAttempts = [1024 * 1024 * 10];
89+
90+
if ($this->useTempFile($file)) {
91+
if ($file->getStorage()->isLocal()) {
92+
// Temp file required but file is local, so retrieve $sizeAttempt bytes first,
93+
// and if it doesn't work, retrieve the entire file.
94+
$sizeAttempts[] = null;
95+
}
96+
} else {
97+
// Temp file is not required and file is local so retrieve entire file.
98+
$sizeAttempts = [null];
7399
}
74-
} else {
75-
// Temp file is not required and file is local so retrieve entire file.
76-
$sizeAttempts = [null];
77-
}
78100

79-
foreach ($sizeAttempts as $size) {
80-
$absPath = false;
81-
// File is remote, generate a sparse file
82-
if (!$file->getStorage()->isLocal()) {
83-
$absPath = $this->getSparseFile($file, $size);
84-
}
85-
// Defaults to existing routine if generating sparse file fails
86-
if ($absPath === false) {
87-
$absPath = $this->getLocalFile($file, $size);
88-
}
89-
if ($absPath === false) {
90-
Server::get(LoggerInterface::class)->error(
91-
'Failed to get local file to generate thumbnail for: ' . $file->getPath(),
92-
['app' => 'core']
93-
);
94-
return null;
95-
}
101+
foreach ($sizeAttempts as $size) {
102+
$absPath = false;
103+
// File is remote, generate a sparse file
104+
if (!$file->getStorage()->isLocal()) {
105+
$absPath = $this->getSparseFile($file, $size);
106+
}
107+
// Defaults to existing routine if generating sparse file fails
108+
if ($absPath === false) {
109+
$absPath = $this->getLocalFile($file, $size);
110+
}
111+
if ($absPath === false) {
112+
Server::get(LoggerInterface::class)->error(
113+
'Failed to get local file to generate thumbnail for: ' . $file->getPath(),
114+
['app' => 'core']
115+
);
116+
return null;
117+
}
118+
119+
// Attempt still image grabs from selected timestamps
120+
foreach ($timeAttempts as $timeStamp) {
121+
$result = $this->generateThumbNail($maxX, $maxY, $absPath, $timeStamp);
122+
if ($result !== null) {
123+
break;
124+
}
125+
Server::get(LoggerInterface::class)->debug(
126+
'Movie preview generation attempt failed'
127+
. ', file=' . $file->getPath()
128+
. ', time=' . $timeStamp
129+
. ', size=' . ($size ?? 'entire file'),
130+
['app' => 'core']
131+
);
132+
}
133+
134+
$this->cleanTmpFiles();
96135

97-
// Attempt still image grabs from selected timestamps
98-
foreach ($timeAttempts as $timeStamp) {
99-
$result = $this->generateThumbNail($maxX, $maxY, $absPath, $timeStamp);
100136
if ($result !== null) {
137+
Server::get(LoggerInterface::class)->debug(
138+
'Movie preview generation attempt success'
139+
. ', file=' . $file->getPath()
140+
. ', time=' . $timeStamp
141+
. ', size=' . ($size ?? 'entire file'),
142+
['app' => 'core']
143+
);
101144
break;
102145
}
103-
Server::get(LoggerInterface::class)->debug(
104-
'Movie preview generation attempt failed'
105-
. ', file=' . $file->getPath()
106-
. ', time=' . $timeStamp
107-
. ', size=' . ($size ?? 'entire file'),
108-
['app' => 'core']
109-
);
110146
}
111-
112-
$this->cleanTmpFiles();
113-
114-
if ($result !== null) {
115-
Server::get(LoggerInterface::class)->debug(
116-
'Movie preview generation attempt success'
117-
. ', file=' . $file->getPath()
118-
. ', time=' . $timeStamp
119-
. ', size=' . ($size ?? 'entire file'),
120-
['app' => 'core']
121-
);
122-
break;
147+
} else {
148+
// HTTP/HTTPS direct connect is available so pass the URL directly to ffmpeg
149+
foreach ($timeAttempts as $timeStamp) {
150+
$result = $this->generateThumbNail($maxX, $maxY, $connectDirect, $timeStamp);
151+
if ($result !== null) {
152+
break;
153+
}
123154
}
124-
125155
}
126156
if ($result === null) {
127157
Server::get(LoggerInterface::class)->error(
@@ -330,7 +360,6 @@ private function generateThumbNail(int $maxX, int $maxY, string $absPath, int $s
330360
}
331361
}
332362

333-
334363
unlink($tmpPath);
335364
return null;
336365
}

0 commit comments

Comments
 (0)