-
-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Store taskprocessing output files with extension to ease mimtype guessing #61689
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
f961edf
1a5d599
de4de30
4e01aaa
9a983cc
950ef2d
c5c7b98
980f236
a501416
b8db1f0
70e7fea
326b94a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -33,6 +33,7 @@ | |||||
| use OCP\TaskProcessing\Exception\PreConditionNotMetException; | ||||||
| use OCP\TaskProcessing\Exception\UnauthorizedException; | ||||||
| use OCP\TaskProcessing\Exception\ValidationException; | ||||||
| use OCP\TaskProcessing\FileShaped; | ||||||
| use OCP\TaskProcessing\IManager; | ||||||
| use OCP\TaskProcessing\ShapeEnumValue; | ||||||
| use OCP\TaskProcessing\Task; | ||||||
|
|
@@ -534,7 +535,8 @@ public function setFileContentsExApp(int $taskId): DataResponse { | |||||
| if (!$handle) { | ||||||
| return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); | ||||||
| } | ||||||
| $fileId = $this->setFileContentsInternal($handle); | ||||||
| $ext = pathinfo($file['name'], PATHINFO_EXTENSION); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| $fileId = $this->setFileContentsInternal($handle, $ext); | ||||||
| return new DataResponse(['fileId' => $fileId], Http::STATUS_CREATED); | ||||||
| } catch (NotFoundException) { | ||||||
| return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND); | ||||||
|
|
@@ -854,14 +856,15 @@ public function getNextScheduledTaskBatch(array $providerIds, array $taskTypeIds | |||||
| * @return int | ||||||
| * @throws NotPermittedException | ||||||
| */ | ||||||
| private function setFileContentsInternal($data): int { | ||||||
| private function setFileContentsInternal($data, string $ext = ''): int { | ||||||
| try { | ||||||
| $folder = $this->appData->getFolder('TaskProcessing'); | ||||||
| } catch (\OCP\Files\NotFoundException) { | ||||||
| $folder = $this->appData->newFolder('TaskProcessing'); | ||||||
| } | ||||||
| $ext = FileShaped::sanitizeExtension($ext); | ||||||
| /** @var SimpleFile $file */ | ||||||
| $file = $folder->newFile(time() . '-' . rand(1, 100000), $data); | ||||||
| $file = $folder->newFile(time() . '-' . rand(1, 100000) . ($ext ? '.' . $ext : ''), $data); | ||||||
| return $file->getId(); | ||||||
|
marcelklehr marked this conversation as resolved.
|
||||||
| } | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -9,13 +9,15 @@ | |||||
|
|
||||||
| namespace OCP\TaskProcessing; | ||||||
|
|
||||||
| use OCP\AppFramework\Attribute\Consumable; | ||||||
| use OCP\TaskProcessing\Exception\ValidationException; | ||||||
|
|
||||||
| /** | ||||||
| * The input and output Shape types | ||||||
| * | ||||||
| * @since 30.0.0 | ||||||
| */ | ||||||
| #[Consumable(since: '30.0.0')] | ||||||
| enum EShapeType: int { | ||||||
| /** | ||||||
| * @since 30.0.0 | ||||||
|
|
@@ -156,28 +158,28 @@ public function validateInput(mixed $value): void { | |||||
| */ | ||||||
| public function validateOutputWithFileData(mixed $value): void { | ||||||
| $this->validateNonFileType($value); | ||||||
| if ($this === EShapeType::Image && !is_string($value)) { | ||||||
| if ($this === EShapeType::Image && !is_string($value) && !($value instanceof FileShaped && $value->getShapeType() === EShapeType::Image)) { | ||||||
| throw new ValidationException('Non-image item provided for Image slot'); | ||||||
| } | ||||||
| if ($this === EShapeType::ListOfImages && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item))) > 0)) { | ||||||
| if ($this === EShapeType::ListOfImages && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item) && !($item instanceof FileShaped && $item->getShapeType() === EShapeType::Image))) > 0)) { | ||||||
|
Comment on lines
-159
to
+164
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe it would be nice to exact these checks in functions with separate something along these lines but feel free to do it your way: /**
* Check whether a single value is valid file output data for the given shape type.
* Accepts raw string data or a FileShaped wrapper whose shape type matches.
*/
private static function isValidFileData(mixed $value, EShapeType $expected): bool {
if (is_string($value)) {
return true;
}
return $value instanceof FileShaped && $value->getShapeType() === $expected;
}
/**
* Check whether a value is a valid list of file output data items.
*/
private static function isValidFileDataList(mixed $value, EShapeType $scalarType): bool {
if (!is_array($value)) {
return false;
}
foreach ($value as $item) {
if (!self::isValidFileData($item, $scalarType)) {
return false;
}
}
return true;
}and can be used like: if ($this === EShapeType::Image && !self::isValidFileData($value, EShapeType::Image)) {
throw new ValidationException('Non-image item provided for Image slot');
} |
||||||
| throw new ValidationException('Non-image list item provided for ListOfImages slot'); | ||||||
| } | ||||||
| if ($this === EShapeType::Audio && !is_string($value)) { | ||||||
| if ($this === EShapeType::Audio && !is_string($value) && !($value instanceof FileShaped && $value->getShapeType() === EShapeType::Audio)) { | ||||||
| throw new ValidationException('Non-audio item provided for Audio slot'); | ||||||
| } | ||||||
| if ($this === EShapeType::ListOfAudios && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item))) > 0)) { | ||||||
| if ($this === EShapeType::ListOfAudios && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item) && !($item instanceof FileShaped && $item->getShapeType() === EShapeType::Audio))) > 0)) { | ||||||
| throw new ValidationException('Non-audio list item provided for ListOfAudio slot'); | ||||||
| } | ||||||
| if ($this === EShapeType::Video && !is_string($value)) { | ||||||
| if ($this === EShapeType::Video && !is_string($value) && !($value instanceof FileShaped && $value->getShapeType() === EShapeType::Video)) { | ||||||
| throw new ValidationException('Non-video item provided for Video slot'); | ||||||
| } | ||||||
| if ($this === EShapeType::ListOfVideos && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item))) > 0)) { | ||||||
| throw new ValidationException('Non-video list item provided for ListOfTexts slot'); | ||||||
| if ($this === EShapeType::ListOfVideos && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item) && !($item instanceof FileShaped && $item->getShapeType() === EShapeType::Video))) > 0)) { | ||||||
| throw new ValidationException('Non-video list item provided for ListOfVideos slot'); | ||||||
| } | ||||||
| if ($this === EShapeType::File && !is_string($value)) { | ||||||
| if ($this === EShapeType::File && !is_string($value) && !($value instanceof FileShaped && $value->getShapeType() === EShapeType::File)) { | ||||||
| throw new ValidationException('Non-file item provided for File slot'); | ||||||
| } | ||||||
| if ($this === EShapeType::ListOfFiles && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item))) > 0)) { | ||||||
| if ($this === EShapeType::ListOfFiles && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item) && !($item instanceof FileShaped && $item->getShapeType() === EShapeType::File))) > 0)) { | ||||||
| throw new ValidationException('Non-audio list item provided for ListOfFiles slot'); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| } | ||||||
| } | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||
| */ | ||
|
|
||
| namespace OCP\TaskProcessing; | ||
|
|
||
| use OCP\AppFramework\Attribute\Consumable; | ||
|
|
||
| /** | ||
| * Data object for file-shaped output entries | ||
| * | ||
| * @since 35.0.0 | ||
| */ | ||
| #[Consumable(since: '35.0.0')] | ||
|
marcelklehr marked this conversation as resolved.
|
||
| final class FileShaped { | ||
| /** | ||
| * @param EShapeType $shapeType | ||
| * @param string $data | ||
| * @param string $extension (optional) | ||
| * | ||
| * @since 35.0.0 | ||
| */ | ||
| public function __construct( | ||
| private EShapeType $shapeType, | ||
| private string $data, | ||
| private string $extension = '', | ||
| ) { | ||
| $this->extension = self::sanitizeExtension($this->extension); | ||
| } | ||
|
|
||
| /** | ||
| * @return string | ||
| * @since 35.0.0 | ||
| */ | ||
| public function getData(): string { | ||
| return $this->data; | ||
| } | ||
|
|
||
| /** | ||
| * @since 35.0.0 | ||
| */ | ||
| public function getShapeType(): EShapeType { | ||
| return $this->shapeType; | ||
| } | ||
|
|
||
| /** | ||
| * @since 35.0.0 | ||
| */ | ||
| public function getExtension(): string { | ||
| return $this->extension; | ||
| } | ||
|
|
||
| /** | ||
| * @since 35.0.0 | ||
| */ | ||
| public static function sanitizeExtension(string $ext): string { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the extension provider is within the trust boundary of ex-apps and php apps but it may not hurt to reject |
||
| if ($ext === '') { | ||
| return ''; | ||
| } | ||
| $ext = str_replace(['.', '/'], '', $ext); | ||
|
marcelklehr marked this conversation as resolved.
|
||
| $ext = preg_replace('/[^A-Za-z0-9]/', '', $ext) ?? $ext; | ||
| $ext = strtolower($ext); | ||
| $ext = substr($ext, 0, 16); | ||
| return $ext; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -9,6 +9,7 @@ | |||
|
|
||||
| namespace OCP\TaskProcessing; | ||||
|
|
||||
| use OCP\AppFramework\Attribute\Implementable; | ||||
| use OCP\Files\File; | ||||
| use OCP\TaskProcessing\Exception\ProcessingException; | ||||
|
|
||||
|
|
@@ -17,6 +18,7 @@ | |||
| * implement a task processing provider | ||||
| * @since 30.0.0 | ||||
| */ | ||||
| #[Implementable(since: '30.0.0')] | ||||
| interface ISynchronousProvider extends IProvider { | ||||
|
|
||||
| /** | ||||
|
|
@@ -25,7 +27,7 @@ interface ISynchronousProvider extends IProvider { | |||
| * @param null|string $userId The user that created the current task | ||||
| * @param array<string, list<numeric|string|File>|numeric|string|File> $input The task input | ||||
| * @param callable(float):bool $reportProgress Report the task progress. If this returns false, that means the task was cancelled and processing should be stopped. | ||||
| * @psalm-return array<string, list<numeric|string>|numeric|string> | ||||
| * @psalm-return array<string, list<numeric|string|FileShaped>|numeric|string|FileShaped> | ||||
|
Comment on lines
-28
to
+30
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe it can be noted that
|
||||
| * @throws ProcessingException | ||||
| * @since 30.0.0 | ||||
| */ | ||||
|
|
||||
Uh oh!
There was an error while loading. Please reload this page.