mirror of
https://github.com/nextcloud/server.git
synced 2026-06-10 17:23:59 -04:00
Merge pull request #53634 from invario/preview-direct-download
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Integration sqlite / changes (push) Waiting to run
Integration sqlite / integration-sqlite (master, 8.4, main, --tags ~@large files_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, capabilities_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, collaboration_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, comments_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, dav_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, federation_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, file_conversions) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, files_reminders) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, filesdrop_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, ldap_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, openldap_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, openldap_numerical_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, remoteapi_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, routing_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, setup_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, sharees_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, sharing_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, theming_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, videoverification_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite-summary (push) Blocked by required conditions
Psalm static code analysis / static-code-analysis (push) Has been cancelled
Psalm static code analysis / static-code-analysis-security (push) Has been cancelled
Psalm static code analysis / static-code-analysis-ocp (push) Has been cancelled
Psalm static code analysis / static-code-analysis-ncu (push) Has been cancelled
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Integration sqlite / changes (push) Waiting to run
Integration sqlite / integration-sqlite (master, 8.4, main, --tags ~@large files_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, capabilities_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, collaboration_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, comments_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, dav_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, federation_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, file_conversions) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, files_reminders) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, filesdrop_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, ldap_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, openldap_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, openldap_numerical_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, remoteapi_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, routing_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, setup_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, sharees_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, sharing_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, theming_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, videoverification_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite-summary (push) Blocked by required conditions
Psalm static code analysis / static-code-analysis (push) Has been cancelled
Psalm static code analysis / static-code-analysis-security (push) Has been cancelled
Psalm static code analysis / static-code-analysis-ocp (push) Has been cancelled
Psalm static code analysis / static-code-analysis-ncu (push) Has been cancelled
feat(previews): allow ffmpeg to connect direct for AWS S3 buckets
This commit is contained in:
commit
cbaf6e73a5
4 changed files with 137 additions and 62 deletions
|
|
@ -28,6 +28,8 @@ class AmazonS3 extends Backend {
|
|||
->setFlag(DefinitionParameter::FLAG_OPTIONAL),
|
||||
(new DefinitionParameter('port', $l->t('Port')))
|
||||
->setFlag(DefinitionParameter::FLAG_OPTIONAL),
|
||||
(new DefinitionParameter('proxy', $l->t('Proxy')))
|
||||
->setFlag(DefinitionParameter::FLAG_OPTIONAL),
|
||||
(new DefinitionParameter('region', $l->t('Region')))
|
||||
->setFlag(DefinitionParameter::FLAG_OPTIONAL),
|
||||
(new DefinitionParameter('storageClass', $l->t('Storage Class')))
|
||||
|
|
@ -42,6 +44,9 @@ class AmazonS3 extends Backend {
|
|||
(new DefinitionParameter('useMultipartCopy', $l->t('Enable multipart copy')))
|
||||
->setType(DefinitionParameter::VALUE_BOOLEAN)
|
||||
->setDefaultValue(true),
|
||||
(new DefinitionParameter('use_presigned_url', $l->t('Use presigned S3 url')))
|
||||
->setType(DefinitionParameter::VALUE_BOOLEAN)
|
||||
->setDefaultValue(false),
|
||||
(new DefinitionParameter('sse_c_key', $l->t('SSE-C encryption key')))
|
||||
->setType(DefinitionParameter::VALUE_PASSWORD)
|
||||
->setFlag(DefinitionParameter::FLAG_OPTIONAL),
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ use OCP\ICache;
|
|||
use OCP\ICacheFactory;
|
||||
use OCP\ITempManager;
|
||||
use OCP\Server;
|
||||
use Override;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class AmazonS3 extends Common {
|
||||
|
|
@ -760,4 +761,44 @@ class AmazonS3 extends Common {
|
|||
|
||||
return $size;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function getDirectDownload(string $path): array|false {
|
||||
if (!$this->isUsePresignedUrl()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$command = $this->getConnection()->getCommand('GetObject', [
|
||||
'Bucket' => $this->bucket,
|
||||
'Key' => $path,
|
||||
]);
|
||||
$expiration = new \DateTimeImmutable('+60 minutes');
|
||||
|
||||
try {
|
||||
// generate a presigned URL that expires after $expiration time
|
||||
$presignedUrl = (string)$this->getConnection()->createPresignedRequest($command, $expiration, [
|
||||
'signPayload' => true,
|
||||
])->getUri();
|
||||
} catch (S3Exception $exception) {
|
||||
$this->logger->error($exception->getMessage(), [
|
||||
'app' => 'files_external',
|
||||
'exception' => $exception,
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
return [
|
||||
'url' => $presignedUrl,
|
||||
'expiration' => $expiration->getTimestamp(),
|
||||
];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function getDirectDownloadById(string $fileId): array|false {
|
||||
if (!$this->isUsePresignedUrl()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$entry = $this->getCache()->get((int)$fileId);
|
||||
return $this->getDirectDownload($entry->getPath());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -298,15 +298,15 @@ trait S3ObjectTrait {
|
|||
}
|
||||
|
||||
public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string {
|
||||
if (!$this->isUsePresignedUrl()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$command = $this->getConnection()->getCommand('GetObject', [
|
||||
'Bucket' => $this->getBucket(),
|
||||
'Key' => $urn,
|
||||
]);
|
||||
|
||||
if (!$this->isUsePresignedUrl()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return (string)$this->getConnection()->createPresignedRequest($command, $expiration, [
|
||||
'signPayload' => true,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,23 @@ class Movie extends ProviderV2 {
|
|||
return is_string($this->binary);
|
||||
}
|
||||
|
||||
private function connectDirect(File $file): string|false {
|
||||
if ($file->isEncrypted()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Checks for availability to access the video file directly via HTTP/HTTPS.
|
||||
// Returns a string containing URL if available. Only implemented and tested
|
||||
// with Amazon S3 currently. In all other cases, return false. ffmpeg
|
||||
// supports other protocols so this function may expand in the future.
|
||||
$gddValues = $file->getStorage()->getDirectDownloadById((string)$file->getId());
|
||||
|
||||
if (is_array($gddValues) && array_key_exists('url', $gddValues)) {
|
||||
return str_starts_with($gddValues['url'], 'http') ? $gddValues['url'] : false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
|
|
@ -54,74 +71,87 @@ class Movie extends ProviderV2 {
|
|||
|
||||
$result = null;
|
||||
|
||||
$connectDirect = $this->connectDirect($file);
|
||||
|
||||
// Timestamps to make attempts to generate a still
|
||||
$timeAttempts = [5, 1, 0];
|
||||
|
||||
// By default, download $sizeAttempts from the file along with
|
||||
// the 'moov' atom.
|
||||
// Example bitrates in the higher range:
|
||||
// 4K HDR H265 60 FPS = 75 Mbps = 9 MB per second needed for a still
|
||||
// 1080p H265 30 FPS = 10 Mbps = 1.25 MB per second needed for a still
|
||||
// 1080p H264 30 FPS = 16 Mbps = 2 MB per second needed for a still
|
||||
$sizeAttempts = [1024 * 1024 * 10];
|
||||
// If HTTP/HTTPS direct connect is not available or if the file is encrypted,
|
||||
// process normally
|
||||
if ($connectDirect === false) {
|
||||
// By default, download $sizeAttempts from the file along with
|
||||
// the 'moov' atom.
|
||||
// Example bitrates in the higher range:
|
||||
// 4K HDR H265 60 FPS = 75 Mbps = 9 MB per second needed for a still
|
||||
// 1080p H265 30 FPS = 10 Mbps = 1.25 MB per second needed for a still
|
||||
// 1080p H264 30 FPS = 16 Mbps = 2 MB per second needed for a still
|
||||
$sizeAttempts = [1024 * 1024 * 10];
|
||||
|
||||
if ($this->useTempFile($file)) {
|
||||
if ($file->getStorage()->isLocal()) {
|
||||
// Temp file required but file is local, so retrieve $sizeAttempt bytes first,
|
||||
// and if it doesn't work, retrieve the entire file.
|
||||
$sizeAttempts[] = null;
|
||||
if ($this->useTempFile($file)) {
|
||||
if ($file->getStorage()->isLocal()) {
|
||||
// Temp file required but file is local, so retrieve $sizeAttempt bytes first,
|
||||
// and if it doesn't work, retrieve the entire file.
|
||||
$sizeAttempts[] = null;
|
||||
}
|
||||
} else {
|
||||
// Temp file is not required and file is local so retrieve entire file.
|
||||
$sizeAttempts = [null];
|
||||
}
|
||||
|
||||
foreach ($sizeAttempts as $size) {
|
||||
$absPath = false;
|
||||
// File is remote, generate a sparse file
|
||||
if (!$file->getStorage()->isLocal()) {
|
||||
$absPath = $this->getSparseFile($file, $size);
|
||||
}
|
||||
// Defaults to existing routine if generating sparse file fails
|
||||
if ($absPath === false) {
|
||||
$absPath = $this->getLocalFile($file, $size);
|
||||
}
|
||||
if ($absPath === false) {
|
||||
Server::get(LoggerInterface::class)->error(
|
||||
'Failed to get local file to generate thumbnail for: ' . $file->getPath(),
|
||||
['app' => 'core']
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Attempt still image grabs from selected timestamps
|
||||
foreach ($timeAttempts as $timeStamp) {
|
||||
$result = $this->generateThumbNail($maxX, $maxY, $absPath, $timeStamp);
|
||||
if ($result !== null) {
|
||||
break;
|
||||
}
|
||||
Server::get(LoggerInterface::class)->debug(
|
||||
'Movie preview generation attempt failed'
|
||||
. ', file=' . $file->getPath()
|
||||
. ', time=' . $timeStamp
|
||||
. ', size=' . ($size ?? 'entire file'),
|
||||
['app' => 'core']
|
||||
);
|
||||
}
|
||||
|
||||
$this->cleanTmpFiles();
|
||||
|
||||
if ($result !== null) {
|
||||
Server::get(LoggerInterface::class)->debug(
|
||||
'Movie preview generation attempt success'
|
||||
. ', file=' . $file->getPath()
|
||||
. ', time=' . $timeStamp
|
||||
. ', size=' . ($size ?? 'entire file'),
|
||||
['app' => 'core']
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Temp file is not required and file is local so retrieve entire file.
|
||||
$sizeAttempts = [null];
|
||||
}
|
||||
|
||||
foreach ($sizeAttempts as $size) {
|
||||
$absPath = false;
|
||||
// File is remote, generate a sparse file
|
||||
if (!$file->getStorage()->isLocal()) {
|
||||
$absPath = $this->getSparseFile($file, $size);
|
||||
}
|
||||
// Defaults to existing routine if generating sparse file fails
|
||||
if ($absPath === false) {
|
||||
$absPath = $this->getLocalFile($file, $size);
|
||||
}
|
||||
if ($absPath === false) {
|
||||
Server::get(LoggerInterface::class)->error(
|
||||
'Failed to get local file to generate thumbnail for: ' . $file->getPath(),
|
||||
['app' => 'core']
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Attempt still image grabs from selected timestamps
|
||||
// HTTP/HTTPS direct connect is available so pass the URL directly to ffmpeg
|
||||
foreach ($timeAttempts as $timeStamp) {
|
||||
$result = $this->generateThumbNail($maxX, $maxY, $absPath, $timeStamp);
|
||||
$result = $this->generateThumbNail($maxX, $maxY, $connectDirect, $timeStamp);
|
||||
if ($result !== null) {
|
||||
break;
|
||||
}
|
||||
Server::get(LoggerInterface::class)->debug(
|
||||
'Movie preview generation attempt failed'
|
||||
. ', file=' . $file->getPath()
|
||||
. ', time=' . $timeStamp
|
||||
. ', size=' . ($size ?? 'entire file'),
|
||||
['app' => 'core']
|
||||
);
|
||||
}
|
||||
|
||||
$this->cleanTmpFiles();
|
||||
|
||||
if ($result !== null) {
|
||||
Server::get(LoggerInterface::class)->debug(
|
||||
'Movie preview generation attempt success'
|
||||
. ', file=' . $file->getPath()
|
||||
. ', time=' . $timeStamp
|
||||
. ', size=' . ($size ?? 'entire file'),
|
||||
['app' => 'core']
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
if ($result === null) {
|
||||
Server::get(LoggerInterface::class)->error(
|
||||
|
|
@ -330,7 +360,6 @@ class Movie extends ProviderV2 {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
unlink($tmpPath);
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue