From 87216f3fb98827fa23009d406b2e05a6f22547cd Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Tue, 10 Dec 2024 16:42:20 +0100 Subject: [PATCH 1/6] Reinstate validation, add resolve for internal schema entities --- lib/Db/Schema.php | 2 +- lib/Service/ObjectService.php | 38 +++++++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/lib/Db/Schema.php b/lib/Db/Schema.php index 571618ce..5c40c430 100644 --- a/lib/Db/Schema.php +++ b/lib/Db/Schema.php @@ -141,7 +141,7 @@ public function getSchemaObject(IURLGenerator $urlGenerator): object // Validator needs this specific $schema $data['$schema'] = 'https://json-schema.org/draft/2020-12/schema'; - $data['$id'] = $urlGenerator->getAbsoluteURL($urlGenerator->linkToRoute('openregister.Schemas.show', ['id' => $this->getUuid()])); + $data['$id'] = $urlGenerator->getAbsoluteURL($urlGenerator->linkToRoute('openregister.Schemas.show', ['id' => $this->getId()])); return json_decode(json_encode($data)); } diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 6356c4b4..1d1cae68 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -23,6 +23,7 @@ use OCP\IURLGenerator; use Opis\JsonSchema\ValidationResult; use Opis\JsonSchema\Validator; +use Opis\Uri\Uri; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; @@ -100,6 +101,29 @@ public function getOpenConnector(string $filePath = '\Service\ObjectService'): m return null; } + /** + * Fetch schema from URL + * + * @param Uri $uri The URI registered by the resolver. + * + * @return string The resulting json object. + */ + public function fetchSchema(Uri $uri):string + { + if ($this->urlGenerator->getBaseUrl() === $uri->scheme().'://'.$uri->host() + && str_contains(haystack: $uri->path(), needle: '/api/schemas') === true + ) { + $exploded = explode(separator: '/', string: $uri->path()); + $schema = $this->schemaMapper->find(end($exploded)); + + return json_encode($schema->getSchemaObject($this->urlGenerator)); + } + + // @TODO: Decide if we also want to accept truly external schemas, and if so, implement them. + + return ''; + } + /** * Validate an object with a schema. * If schema is not given and schemaObject is filled, the object will validate to the schemaObject. @@ -119,6 +143,8 @@ public function validateObject(array $object, ?int $schemaId = null, object $sch $validator = new Validator(); $validator->setMaxErrors(100); $validator->parser()->getFormatResolver()->register('string', 'bsn', new BsnFormat()); + $validator->loader()->resolver()->registerProtocol('http', [$this, 'fetchSchema']); + return $validator->validate(data: json_decode(json_encode($object)), schema: $schemaObject); @@ -374,7 +400,7 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt ); } -// $validationResult = $this->validateObject(object: $object, schemaId: $schema); + $validationResult = $this->validateObject(object: $object, schemaId: $schema); // Create new entity if none exists if (isset($object['id']) === false || $objectEntity === null) { @@ -413,17 +439,17 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt $objectEntity->setUri($this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('openregister.Objects.show', ['id' => $objectEntity->getUuid()]))); - if ($objectEntity->getId()) {// && ($schemaObject->getHardValidation() === false || $validationResult->isValid() === true)){ + if ($objectEntity->getId() && ($schemaObject->getHardValidation() === false || $validationResult->isValid() === true)){ $objectEntity = $this->objectEntityMapper->update($objectEntity); $this->auditTrailMapper->createAuditTrail(new: $objectEntity, old: $oldObject); - } else {//if ($schemaObject->getHardValidation() === false || $validationResult->isValid() === true) { + } else if ($schemaObject->getHardValidation() === false || $validationResult->isValid() === true) { $objectEntity = $this->objectEntityMapper->insert($objectEntity); $this->auditTrailMapper->createAuditTrail(new: $objectEntity); } -// if ($validationResult->isValid() === false) { -// throw new ValidationException(message: 'The object could not be validated', errors: $validationResult->error()); -// } + if ($validationResult->isValid() === false) { + throw new ValidationException(message: 'The object could not be validated', errors: $validationResult->error()); + } return $objectEntity; } From 80889795890e6f79a5697f4d1c9f9078dd64c858 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Wed, 11 Dec 2024 16:48:46 +0100 Subject: [PATCH 2/6] validation of files, oneof etc. first draft --- lib/Db/File.php | 130 +++++++++++++ lib/Db/FileMapper.php | 104 ++++++++++ lib/Db/Schema.php | 17 ++ lib/Service/ObjectService.php | 352 ++++++++++++++++++++++++---------- 4 files changed, 498 insertions(+), 105 deletions(-) create mode 100644 lib/Db/File.php create mode 100644 lib/Db/FileMapper.php diff --git a/lib/Db/File.php b/lib/Db/File.php new file mode 100644 index 00000000..0c3f57d7 --- /dev/null +++ b/lib/Db/File.php @@ -0,0 +1,130 @@ +addType('uuid', 'string'); + $this->addType('filename', 'string'); + $this->addType('downloadUrl', 'string'); + $this->addType('shareUrl', 'string'); + $this->addType('accessUrl', 'string'); + $this->addType('extension', 'string'); + $this->addType('checksum', 'string'); + $this->addType('source', 'int'); + $this->addType('userId', 'string'); + $this->addType('created', 'datetime'); + $this->addType('updated', 'datetime'); + } + + public function getJsonFields(): array + { + return array_keys( + array_filter($this->getFieldTypes(), function ($field) { + return $field === 'json'; + }) + ); + } + + public function hydrate(array $object): self + { + $jsonFields = $this->getJsonFields(); + + foreach ($object as $key => $value) { + if (in_array($key, $jsonFields) === true && $value === []) { + $value = []; + } + + $method = 'set'.ucfirst($key); + + try { + $this->$method($value); + } catch (\Exception $exception) { +// ("Error writing $key"); + } + } + + return $this; + } + + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'filename' => $this->filename, + 'downloadUrl' => $this->downloadUrl, + 'shareUrl' => $this->shareUrl, + 'accessUrl' => $this->accessUrl, + 'extension' => $this->extension, + 'checksum' => $this->checksum, + 'source' => $this->source, + 'userId' => $this->userId, + 'created' => isset($this->created) ? $this->created->format('c') : null, + 'updated' => isset($this->updated) ? $this->updated->format('c') : null, + ]; + } + + public static function getSchema(IURLGenerator $IURLGenerator): string { + return json_encode([ + '$id' => $IURLGenerator->getBaseUrl().'/apps/openconnector/api/files/schema', + '$schema' => 'https://json-schema.org/draft/2020-12/schema', + 'type' => 'object', + 'required' => [ + 'filename', + 'accessUrl', + 'userId', + ], + 'properties' => [ + 'filename' => [ + 'type' => 'string', + 'minLength' => 1, + 'maxLength' => 255 + ], + 'downloadUrl' => [ + 'type' => 'string', + 'format' => 'uri', + ], + 'shareUrl' => [ + 'type' => 'string', + 'format' => 'uri', + ], + 'accessUrl' => [ + 'type' => 'string', + 'format' => 'uri', + ], + 'extension' => [ + 'type' => 'string', + 'maxLength' => 10, + ], + 'checksum' => [ + 'type' => 'string', + ], + 'source' => [ + 'type' => 'number', + ], + 'userId' => [ + 'type' => 'string', + ] + ] + ]); + } +} diff --git a/lib/Db/FileMapper.php b/lib/Db/FileMapper.php new file mode 100644 index 00000000..e3e29ff9 --- /dev/null +++ b/lib/Db/FileMapper.php @@ -0,0 +1,104 @@ +db->getQueryBuilder(); + + $qb->select('*') + ->from('openconnector_jobs') + ->where( + $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) + ); + + return $this->findEntity(query: $qb); + } + + public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from('openconnector_jobs') + ->setMaxResults($limit) + ->setFirstResult($offset); + + foreach ($filters as $filter => $value) { + if ($value === 'IS NOT NULL') { + $qb->andWhere($qb->expr()->isNotNull($filter)); + } elseif ($value === 'IS NULL') { + $qb->andWhere($qb->expr()->isNull($filter)); + } else { + $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); + } + } + + if (empty($searchConditions) === false) { + $qb->andWhere('(' . implode(' OR ', $searchConditions) . ')'); + foreach ($searchParams as $param => $value) { + $qb->setParameter($param, $value); + } + } + + return $this->findEntities(query: $qb); + } + + public function createFromArray(array $object): File + { + $obj = new File(); + $obj->hydrate($object); + // Set uuid + if ($obj->getUuid() === null){ + $obj->setUuid(Uuid::v4()); + } + return $this->insert(entity: $obj); + } + + public function updateFromArray(int $id, array $object): File + { + $obj = $this->find($id); + $obj->hydrate($object); + + // Set or update the version + $version = explode('.', $obj->getVersion()); + $version[2] = (int)$version[2] + 1; + $obj->setVersion(implode('.', $version)); + + return $this->update($obj); + } + + /** + * Get the total count of all call logs. + * + * @return int The total number of call logs in the database. + */ + public function getTotalCallCount(): int + { + $qb = $this->db->getQueryBuilder(); + + // Select count of all logs + $qb->select($qb->createFunction('COUNT(*) as count')) + ->from('openconnector_jobs'); + + $result = $qb->execute(); + $row = $result->fetch(); + + // Return the total count + return (int)$row['count']; + } +} diff --git a/lib/Db/Schema.php b/lib/Db/Schema.php index 5c40c430..15c52670 100644 --- a/lib/Db/Schema.php +++ b/lib/Db/Schema.php @@ -132,6 +132,23 @@ public function getSchemaObject(IURLGenerator $urlGenerator): object foreach ($data['properties'] as $title => $property) { // Remove empty fields with array_filter(). $data['properties'][$title] = array_filter($property); + + if($property['type'] === 'file') { + $data['properties'][$title] = ['$ref' => $urlGenerator->getBaseUrl().'/apps/openregister/api/files/schema']; + } + if($property['type'] === 'oneOf') { + unset($data['properties'][$title]['type']); + $data['properties'][$title]['oneOf'] = array_map( + callback: function (array $item) use ($urlGenerator) { + if($item['type'] === 'file') { + unset($item['type']); + $item['$ref'] = $urlGenerator->getBaseUrl().'/apps/openregister/api/files/schema'; + } + + return $item; + }, + array: $property['oneOf']); + } } unset($data['id'], $data['uuid'], $data['summary'], $data['archive'], $data['source'], diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 1d1cae68..0dd2075e 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -7,6 +7,7 @@ use GuzzleHttp\Exception\GuzzleException; use InvalidArgumentException; use OC\URLGenerator; +use OCA\OpenRegister\Db\File; use OCA\OpenRegister\Db\Source; use OCA\OpenRegister\Db\SourceMapper; use OCA\OpenRegister\Db\Schema; @@ -20,7 +21,9 @@ use OCA\OpenRegister\Exception\ValidationException; use OCA\OpenRegister\Formats\BsnFormat; use OCP\App\IAppManager; +use OCP\IAppConfig; use OCP\IURLGenerator; +use Opis\JsonSchema\Errors\ErrorFormatter; use Opis\JsonSchema\ValidationResult; use Opis\JsonSchema\Validator; use Opis\Uri\Uri; @@ -71,7 +74,8 @@ public function __construct( private ContainerInterface $container, private readonly IURLGenerator $urlGenerator, private readonly FileService $fileService, - private readonly IAppManager $appManager + private readonly IAppManager $appManager, + private readonly IAppConfig $config, ) { $this->objectEntityMapper = $objectEntityMapper; @@ -105,10 +109,12 @@ public function getOpenConnector(string $filePath = '\Service\ObjectService'): m * Fetch schema from URL * * @param Uri $uri The URI registered by the resolver. - * + * * @return string The resulting json object. + * + * @throws GuzzleException */ - public function fetchSchema(Uri $uri):string + public function resolveSchema(Uri $uri): string { if ($this->urlGenerator->getBaseUrl() === $uri->scheme().'://'.$uri->host() && str_contains(haystack: $uri->path(), needle: '/api/schemas') === true @@ -119,7 +125,21 @@ public function fetchSchema(Uri $uri):string return json_encode($schema->getSchemaObject($this->urlGenerator)); } - // @TODO: Decide if we also want to accept truly external schemas, and if so, implement them. + if ($this->urlGenerator->getBaseUrl() === $uri->scheme().'://'.$uri->host() + && str_contains(haystack: $uri->path(), needle: '/api/files/schema') === true + ) { + $exploded = explode(separator: '/', string: $uri->path()); + return File::getSchema($this->urlGenerator); + } + + // @TODO: Validate file schema + + if ($this->config->getValueBool(app: 'openregister', key: 'allowExternalSchemas') === true) { + $client = new Client(); + $result = $client->get(\GuzzleHttp\Psr7\Uri::fromParts($uri->components())); + + return $result->getBody()->getContents(); + } return ''; } @@ -140,10 +160,14 @@ public function validateObject(array $object, ?int $schemaId = null, object $sch $schemaObject = $this->schemaMapper->find($schemaId)->getSchemaObject($this->urlGenerator); } + if($schemaObject->properties === []) { + $schemaObject->properties = new stdClass(); + } + $validator = new Validator(); $validator->setMaxErrors(100); $validator->parser()->getFormatResolver()->register('string', 'bsn', new BsnFormat()); - $validator->loader()->resolver()->registerProtocol('http', [$this, 'fetchSchema']); + $validator->loader()->resolver()->registerProtocol('http', [$this, 'resolveSchema']); return $validator->validate(data: json_decode(json_encode($object)), schema: $schemaObject); @@ -503,6 +527,216 @@ private function handleLinkRelations(ObjectEntity $objectEntity): ObjectEntity return $objectEntity; } + private function addObject + ( + array $property, + string $propertyName, + array $item, + ObjectEntity $objectEntity, + int $register, + int $schema, + ?int $index = null, + ): string + { + $subSchema = $schema; + if(is_int($property['$ref']) === true) { + $subSchema = $property['$ref']; + } else if (filter_var(value: $property['$ref'], filter: FILTER_VALIDATE_URL) !== false) { + $parsedUrl = parse_url($property['$ref']); + $explodedPath = explode(separator: '/', string: $parsedUrl['path']); + $subSchema = end($explodedPath); + } + + // Handle nested object in array + $nestedObject = $this->saveObject( + register: $register, + schema: $subSchema, + object: $item + ); + + if($index === null) { + // Store relation and replace with reference + $relations = $objectEntity->getRelations() ?? []; + $relations[$propertyName] = $nestedObject->getUri(); + $objectEntity->setRelations($relations); + } else { + $relations = $objectEntity->getRelations() ?? []; + $relations[$propertyName . '.' . $index] = $nestedObject->getUri(); + $objectEntity->setRelations($relations); + } + + return $nestedObject->getUri(); + } + + private function handleObjectProperty( + array $property, + string $propertyName, + array $item, + ObjectEntity $objectEntity, + int $register, + int $schema + ): string + { + return $this->addObject( + property: $property,propertyName: $propertyName, item: $item, objectEntity: $objectEntity, register: $register, schema: $schema + ); + } + + private function handleArrayProperty( + array $property, + string $propertyName, + array $items, + ObjectEntity $objectEntity, + int $register, + int $schema + ): array + { + if(isset($property['items']) === false) { + return $items; + } + + if(isset($property['items']['oneOf'])) { + foreach($items as $index=>$item) { + $items[$index] = $this->handleOneOfProperty( + property: $property['items'], + propertyName: $propertyName, + item: $item, + objectEntity: $objectEntity, + register: $register, + schema: $schema + ); + } + return $items; + } + + if ($property['items']['type'] !== 'object' + && $property['items']['type'] !== 'file' + ) { + return $items; + } + + if ($property['items']['type'] === 'file') + { + foreach($items as $index => $item) { + $items[$index] = $this->handleFileProperty( + objectEntity: $objectEntity, + object: [$propertyName => [$index => $item]], + propertyName: $propertyName . '.' . $index + )[$propertyName]; + } + return $items; + } + + foreach($items as $index=>$item) { + $items[$index] = $this->addObject( + property: $property['items'], + propertyName: $propertyName, + item: $item, + objectEntity: $objectEntity, + register: $register, + schema: $schema, + index: $index + ); + } + + return $items; + } + + private function handleOneOfProperty( + array $property, + string $propertyName, + string|array $item, + ObjectEntity $objectEntity, + int $register, + int $schema, + ?int $index = null + ): string|array + { + if (array_is_list($property) === false) { + return $item; + } + + if (array_column(array: $property, column_key: '$ref') === []) { + return $item; + } + + if (is_array($item) === false) { + return $item; + } + + $oneOf = array_filter( + array: $property, + callback: function (array $option) { + return isset($option['$ref']); + } + )[0]; + + return $this->addObject( + property: $oneOf, + propertyName: $propertyName, + item: $item, + objectEntity: $objectEntity, + register: $register, + schema: $schema, + index: $index + ); + } + + private function handleProperty ( + array $property, + string $propertyName, + int $register, + int $schema, + array $object, + ObjectEntity $objectEntity + ): ObjectEntity + { + switch($property['type']) { + case 'object': + $object[$propertyName] = $this->handleObjectProperty( + property: $property, + propertyName: $propertyName, + item: $object[$propertyName], + objectEntity: $objectEntity, + register: $register, + schema: $schema, + ); + break; + case 'array': + $object[$propertyName] = $this->handleArrayProperty( + property: $property, + propertyName: $propertyName, + items: $object[$propertyName], + objectEntity: $objectEntity, + register: $register, + schema: $schema, + ); + break; + case 'oneOf': + $object[$propertyName] = $this->handleOneOfProperty( + property: $property, + propertyName: $propertyName, + item: $object[$propertyName], + objectEntity: $objectEntity, + register: $register, + schema: $schema,); + break; + case 'file': + $object[$propertyName] = $this->handleFileProperty( + objectEntity: $objectEntity, + object: $object, + propertyName: $propertyName + ); + default: + break; + } + + $objectEntity->setObject($object); + + return $objectEntity; + } + + /** * Handle object relations and file properties in schema properties and array items * @@ -524,107 +758,15 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object continue; } - // Handle array type with items that may contain objects/files - if ($property['type'] === 'array' && isset($property['items']) === true) { - // Skip if not array in data - if (is_array($object[$propertyName]) === false) { - continue; - } - - // Process each array item - foreach ($object[$propertyName] as $index => $item) { - if ($property['items']['type'] === 'object') { - $subSchema = $schema; - - if(is_int($property['items']['$ref']) === true) { - $subSchema = $property['items']['$ref']; - } else if (filter_var(value: $property['items']['$ref'], filter: FILTER_VALIDATE_URL) !== false) { - $parsedUrl = parse_url($property['items']['$ref']); - $explodedPath = explode(separator: '/', string: $parsedUrl['path']); - $subSchema = end($explodedPath); - } - - if(is_array($item) === true) { - // Handle nested object in array - $nestedObject = $this->saveObject( - register: $register, - schema: $subSchema, - object: $item - ); - - // Store relation and replace with reference - $relations = $objectEntity->getRelations() ?? []; - $relations[$propertyName . '.' . $index] = $nestedObject->getUri(); - $objectEntity->setRelations($relations); - $object[$propertyName][$index] = $nestedObject->getUri(); - - } else { - $relations = $objectEntity->getRelations() ?? []; - $relations[$propertyName . '.' . $index] = $item; - $objectEntity->setRelations($relations); - } - - } else if ($property['items']['type'] === 'file') { - // Handle file in array - $object[$propertyName][$index] = $this->handleFileProperty( - objectEntity: $objectEntity, - object: [$propertyName => [$index => $item]], - propertyName: $propertyName . '.' . $index - )[$propertyName]; - } - } - } - - // Handle single object type - else if ($property['type'] === 'object') { - - $subSchema = $schema; - - // $ref is a int, id or uuid - if (is_int($property['$ref']) === true - || is_numeric($property['$ref']) === true - || preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $property['$ref']) === 1 - ) { - $subSchema = $property['$ref']; - } else if (filter_var(value: $property['$ref'], filter: FILTER_VALIDATE_URL) !== false) { - $parsedUrl = parse_url($property['$ref']); - $explodedPath = explode(separator: '/', string: $parsedUrl['path']); - $subSchema = end($explodedPath); - } - - if(is_array($object[$propertyName]) === true) { - $nestedObject = $this->saveObject( - register: $register, - schema: $subSchema, - object: $object[$propertyName] - ); - - // Store relation and replace with reference - $relations = $objectEntity->getRelations() ?? []; - $relations[$propertyName] = $nestedObject->getUri(); - $objectEntity->setRelations($relations); - $object[$propertyName] = $nestedObject->getUri(); - - } else { - $relations = $objectEntity->getRelations() ?? []; - $relations[$propertyName] = $object[$propertyName]; - $objectEntity->setRelations($relations); - } - - } - // Handle single file type - else if ($property['type'] === 'file') { - - $object = $this->handleFileProperty( - objectEntity: $objectEntity, - object: $object, - propertyName: $propertyName - ); - } + $objectEntity = $this->handleProperty( + property: $property, + propertyName: $propertyName, + register: $register, + schema: $schema, + object: $object, + objectEntity: $objectEntity, + ); } - - $objectEntity->setObject($object); - return $objectEntity; } From 03a4bb7cf5405fd2c9a224623ac6c1ef413fbc4e Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Thu, 12 Dec 2024 12:51:07 +0100 Subject: [PATCH 3/6] Major cleanup on subobjects --- lib/Db/File.php | 1 - lib/Db/Schema.php | 5 +++ lib/Service/ObjectService.php | 60 ++++++++++++++++++++--------------- 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/lib/Db/File.php b/lib/Db/File.php index 0c3f57d7..18fdb7c7 100644 --- a/lib/Db/File.php +++ b/lib/Db/File.php @@ -91,7 +91,6 @@ public static function getSchema(IURLGenerator $IURLGenerator): string { 'required' => [ 'filename', 'accessUrl', - 'userId', ], 'properties' => [ 'filename' => [ diff --git a/lib/Db/Schema.php b/lib/Db/Schema.php index 15c52670..937d8e42 100644 --- a/lib/Db/Schema.php +++ b/lib/Db/Schema.php @@ -149,6 +149,11 @@ public function getSchemaObject(IURLGenerator $urlGenerator): object }, array: $property['oneOf']); } + if($property['type'] === 'array' + && isset($property['items']['type']) === true + && $property['items']['type'] === 'oneOf') { + unset($data['properties'][$title]['items']['type']); + } } unset($data['id'], $data['uuid'], $data['summary'], $data['archive'], $data['source'], diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 0dd2075e..cbdf149c 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -458,7 +458,6 @@ public function saveObject(int $register, int $schema, array $object): ObjectEnt // Handle object properties that are either nested objects or files if ($schemaObject->getProperties() !== null && is_array($schemaObject->getProperties()) === true) { $objectEntity = $this->handleObjectRelations($objectEntity, $object, $schemaObject->getProperties(), $register, $schema); - $objectEntity->setObject($object); } $objectEntity->setUri($this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('openregister.Objects.show', ['id' => $objectEntity->getUuid()]))); @@ -565,7 +564,7 @@ private function addObject $objectEntity->setRelations($relations); } - return $nestedObject->getUri(); + return $nestedObject->getUuid(); } private function handleObjectProperty( @@ -598,12 +597,13 @@ private function handleArrayProperty( if(isset($property['items']['oneOf'])) { foreach($items as $index=>$item) { $items[$index] = $this->handleOneOfProperty( - property: $property['items'], + property: $property['items']['oneOf'], propertyName: $propertyName, item: $item, objectEntity: $objectEntity, register: $register, - schema: $schema + schema: $schema, + index: $index ); } return $items; @@ -656,6 +656,27 @@ private function handleOneOfProperty( return $item; } + if (in_array(needle:'file', haystack: array_column(array: $property, column_key: 'type')) === true + && is_array($item) === true + && $index !== null + ) { + return $this->handleFileProperty( + objectEntity: $objectEntity, + object: [$propertyName => [$index => $item]], + propertyName: $propertyName + ); + } + if (in_array(needle:'file', haystack: array_column(array: $property, column_key: 'type')) === true + && is_array($item) === true + && $index === null + ) { + return $this->handleFileProperty( + objectEntity: $objectEntity, + object: [$propertyName => $item], + propertyName: $propertyName + ); + } + if (array_column(array: $property, column_key: '$ref') === []) { return $item; } @@ -667,7 +688,7 @@ private function handleOneOfProperty( $oneOf = array_filter( array: $property, callback: function (array $option) { - return isset($option['$ref']); + return isset($option['$ref']) === true; } )[0]; @@ -689,7 +710,7 @@ private function handleProperty ( int $schema, array $object, ObjectEntity $objectEntity - ): ObjectEntity + ): array { switch($property['type']) { case 'object': @@ -714,12 +735,12 @@ private function handleProperty ( break; case 'oneOf': $object[$propertyName] = $this->handleOneOfProperty( - property: $property, + property: $property['oneOf'], propertyName: $propertyName, item: $object[$propertyName], objectEntity: $objectEntity, register: $register, - schema: $schema,); + schema: $schema); break; case 'file': $object[$propertyName] = $this->handleFileProperty( @@ -731,9 +752,7 @@ private function handleProperty ( break; } - $objectEntity->setObject($object); - - return $objectEntity; + return $object; } @@ -758,7 +777,7 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object continue; } - $objectEntity = $this->handleProperty( + $object = $this->handleProperty( property: $property, propertyName: $propertyName, register: $register, @@ -767,6 +786,9 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object objectEntity: $objectEntity, ); } + + $objectEntity->setObject($object); + return $objectEntity; } @@ -785,18 +807,6 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s $fileName = str_replace('.', '_', $propertyName); $objectDot = new Dot($object); - // Check if it's a Nextcloud file URL -// if (str_starts_with($object[$propertyName], $this->urlGenerator->getAbsoluteURL())) { -// $urlPath = parse_url($object[$propertyName], PHP_URL_PATH); -// if (preg_match('/\/f\/(\d+)/', $urlPath, $matches)) { -// $files = $objectEntity->getFiles() ?? []; -// $files[$propertyName] = (int)$matches[1]; -// $objectEntity->setFiles($files); -// $object[$propertyName] = (int)$matches[1]; -// return $object; -// } -// } - // Handle base64 encoded file if (is_string($objectDot->get($propertyName)) === true && preg_match('/^data:([^;]*);base64,(.*)/', $objectDot->get($propertyName), $matches) @@ -809,7 +819,7 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s // Handle URL file else { // Encode special characters in the URL - $encodedUrl = rawurlencode($objectDot->get("$propertyName.downloadUrl")); //@todo hardcoded .downloadUrl + $encodedUrl = rawurlencode($objectDot->get("$propertyName.accessUrl")); //@todo hardcoded .downloadUrl // Decode valid path separators and reserved characters $encodedUrl = str_replace(['%2F', '%3A', '%28', '%29'], ['/', ':', '(', ')'], $encodedUrl); From 6f06d31d5df03b0a332be32ed6e4fe6652d9cf78 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Thu, 12 Dec 2024 13:49:48 +0100 Subject: [PATCH 4/6] Add docblocks to new functions --- lib/Service/ObjectService.php | 67 +++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index cbdf149c..86b883de 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -526,6 +526,20 @@ private function handleLinkRelations(ObjectEntity $objectEntity): ObjectEntity return $objectEntity; } + /** + * Adds a subobject based upon given parameters and adds it to the main object. + * + * @param array $property The property to handle + * @param string $propertyName The name of the property + * @param array $item The contents of the property + * @param ObjectEntity $objectEntity The objectEntity the data belongs to + * @param int $register The register connected to the objectEntity + * @param int $schema The schema connected to the objectEntity + * @param int|null $index If the subobject is in an array, the index of the object in the array. + * + * @return string The updated item + * @throws ValidationException + */ private function addObject ( array $property, @@ -567,6 +581,18 @@ private function addObject return $nestedObject->getUuid(); } + /** + * Handles a property that is of the type array. + * + * @param array $property The property to handle + * @param string $propertyName The name of the property + * @param array $item The contents of the property + * @param ObjectEntity $objectEntity The objectEntity the data belongs to + * @param int $register The register connected to the objectEntity + * @param int $schema The schema connected to the objectEntity + * + * @return string The updated item + */ private function handleObjectProperty( array $property, string $propertyName, @@ -581,6 +607,19 @@ private function handleObjectProperty( ); } + /** + * Handles a property that is of the type array. + * + * @param array $property The property to handle + * @param string $propertyName The name of the property + * @param array $items The contents of the property + * @param ObjectEntity $objectEntity The objectEntity the data belongs to + * @param int $register The register connected to the objectEntity + * @param int $schema The schema connected to the objectEntity + * + * @return array The updated item + * @throws GuzzleException + */ private function handleArrayProperty( array $property, string $propertyName, @@ -642,6 +681,20 @@ private function handleArrayProperty( return $items; } + /** + * Handles a property that of the type oneOf. + * + * @param array $property The property to handle + * @param string $propertyName The name of the property + * @param string|array $item The contents of the property + * @param ObjectEntity $objectEntity The objectEntity the data belongs to + * @param int $register The register connected to the objectEntity + * @param int $schema The schema connected to the objectEntity + * @param int|null $index If the oneOf is in an array, the index within the array + * + * @return string|array The updated item + * @throws GuzzleException + */ private function handleOneOfProperty( array $property, string $propertyName, @@ -703,6 +756,20 @@ private function handleOneOfProperty( ); } + /** + * Rewrites subobjects stored in separate objectentities to the Uuid of that object, + * rewrites files to the chosen format + * + * @param array $property The content of the property in the schema + * @param string $propertyName The name of the property + * @param int $register The register the main object is in + * @param int $schema The schema of the main object + * @param array $object The object to rewrite + * @param ObjectEntity $objectEntity The objectEntity to write the object in + * + * @return array The resulting object + * @throws GuzzleException + */ private function handleProperty ( array $property, string $propertyName, From addef18d8db820cc21c7f754b9518fd4946d7e29 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Thu, 12 Dec 2024 16:55:45 +0100 Subject: [PATCH 5/6] Refactor on file synchronization --- lib/Db/File.php | 23 ++-- lib/Service/ObjectService.php | 234 +++++++++++++++++++++------------- 2 files changed, 161 insertions(+), 96 deletions(-) diff --git a/lib/Db/File.php b/lib/Db/File.php index 18fdb7c7..790ce6d6 100644 --- a/lib/Db/File.php +++ b/lib/Db/File.php @@ -10,14 +10,16 @@ class File extends Entity implements JsonSerializable { protected string $uuid; - protected string $filename; - protected string $downloadUrl; - protected string $shareUrl; - protected string $accessUrl; - protected string $extension; - protected string $checksum; - protected string $source; - protected string $userId; + protected ?string $filename = null; + protected ?string $downloadUrl = null; + protected ?string $shareUrl = null; + protected ?string $accessUrl = null; + protected ?string $extension = null; + protected ?string $checksum = null; + protected ?string $source = null; + protected ?string $userId = null; + protected ?string $base64 = null; + protected ?string $filePath = null; protected DateTime $created; protected DateTime $updated; @@ -89,8 +91,6 @@ public static function getSchema(IURLGenerator $IURLGenerator): string { '$schema' => 'https://json-schema.org/draft/2020-12/schema', 'type' => 'object', 'required' => [ - 'filename', - 'accessUrl', ], 'properties' => [ 'filename' => [ @@ -122,6 +122,9 @@ public static function getSchema(IURLGenerator $IURLGenerator): string { ], 'userId' => [ 'type' => 'string', + ], + 'base64' => [ + 'type' => 'string' ] ] ]); diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 86b883de..25f0b22c 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -3,11 +3,13 @@ namespace OCA\OpenRegister\Service; use Adbar\Dot; +use DateTime; use Exception; use GuzzleHttp\Exception\GuzzleException; use InvalidArgumentException; use OC\URLGenerator; use OCA\OpenRegister\Db\File; +use OCA\OpenRegister\Db\FileMapper; use OCA\OpenRegister\Db\Source; use OCA\OpenRegister\Db\SourceMapper; use OCA\OpenRegister\Db\Schema; @@ -76,6 +78,7 @@ public function __construct( private readonly FileService $fileService, private readonly IAppManager $appManager, private readonly IAppConfig $config, + private readonly FileMapper $fileMapper, ) { $this->objectEntityMapper = $objectEntityMapper; @@ -859,88 +862,10 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object return $objectEntity; } - /** - * Handle file property processing - * - * @param ObjectEntity $objectEntity The object entity - * @param array $object The object data - * @param string $propertyName The name of the file property - * - * @return array Updated object data - * @throws Exception|GuzzleException When file handling fails - */ - private function handleFileProperty(ObjectEntity $objectEntity, array $object, string $propertyName): array - { - $fileName = str_replace('.', '_', $propertyName); - $objectDot = new Dot($object); - - // Handle base64 encoded file - if (is_string($objectDot->get($propertyName)) === true - && preg_match('/^data:([^;]*);base64,(.*)/', $objectDot->get($propertyName), $matches) - ) { - $fileContent = base64_decode($matches[2], true); - if ($fileContent === false) { - throw new Exception('Invalid base64 encoded file'); - } - } - // Handle URL file - else { - // Encode special characters in the URL - $encodedUrl = rawurlencode($objectDot->get("$propertyName.accessUrl")); //@todo hardcoded .downloadUrl - - // Decode valid path separators and reserved characters - $encodedUrl = str_replace(['%2F', '%3A', '%28', '%29'], ['/', ':', '(', ')'], $encodedUrl); - - if (filter_var($encodedUrl, FILTER_VALIDATE_URL)) { - try { - // @todo hacky tacky - // Regular expression to get the filename and extension from url //@todo hardcoded .downloadUrl - if (preg_match("/\/([^\/]+)'\)\/\\\$value$/", $objectDot->get("$propertyName.downloadUrl"), $matches)) { - // @todo hardcoded way of getting the filename and extension from the url - $fileNameFromUrl = $matches[1]; - // @todo use only the extension from the url ? - // $fileName = $fileNameFromUrl; - $extension = substr(strrchr($fileNameFromUrl, '.'), 1); - $fileName = "$fileName.$extension"; - } - - if ($objectDot->has("$propertyName.source") === true) { - $sourceMapper = $this->getOpenConnector(filePath: '\Db\SourceMapper'); - $source = $sourceMapper->find($objectDot->get("$propertyName.source")); - - $callService = $this->getOpenConnector(filePath: '\Service\CallService'); - if ($callService === null) { - throw new Exception("OpenConnector service not available"); - } - $endpoint = str_replace($source->getLocation(), "", $encodedUrl); - - $endpoint = urldecode($endpoint); - - $response = $callService->call(source: $source, endpoint: $endpoint, method: 'GET')->getResponse(); - - $fileContent = $response['body']; - - if( - $response['encoding'] === 'base64' - ) { - $fileContent = base64_decode(string: $fileContent); - } - - } else { - $client = new \GuzzleHttp\Client(); - $response = $client->get($encodedUrl); - $fileContent = $response->getBody()->getContents(); - } - } catch (Exception|NotFoundExceptionInterface $e) { - throw new Exception('Failed to download file from URL: ' . $e->getMessage()); - } - } else if (str_contains($objectDot->get($propertyName), $this->urlGenerator->getBaseUrl()) === true) { - return $object; - } else { - throw new Exception('Invalid file format - must be base64 encoded or valid URL'); - } - } + private function writeFile(string $fileContent, string $propertyName, ObjectEntity $objectEntity, File $file): File + { + $fileName = $file->getFileName(); try { $schema = $this->schemaMapper->find($objectEntity->getSchema()); @@ -964,9 +889,11 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s // Create or find ShareLink $share = $this->fileService->findShare(path: $filePath); if ($share !== null) { - $shareLink = $this->fileService->getShareLink($share).'/download'; + $shareLink = $this->fileService->getShareLink($share); + $downloadLink = $shareLink.'/download'; } else { - $shareLink = $this->fileService->createShareLink(path: $filePath).'/download'; + $shareLink = $this->fileService->createShareLink(path: $filePath); + $downloadLink = $shareLink.'/download'; } $filesDot = new Dot($objectEntity->getFiles() ?? []); @@ -974,13 +901,148 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s $objectEntity->setFiles($filesDot->all()); // Preserve the original uri in the object 'json blob' - $objectDot = $objectDot->set($propertyName, $shareLink); - $object = $objectDot->all(); + $file->setDownloadUrl($downloadLink); + $file->setShareUrl($shareLink); } catch (Exception $e) { throw new Exception('Failed to store file: ' . $e->getMessage()); } - return $object; + return $file; + } + + private function setExtension(File $file): File + { + // Regular expression to get the filename and extension from url + if ($file->getExtension() === false && preg_match("/\/([^\/]+)'\)\/\\\$value$/", $file->getAccessUrl(), $matches)) { + $fileNameFromUrl = $matches[1]; + $file->setExtension(substr(strrchr($fileNameFromUrl, '.'), 1)); + } + + return $file; + } + private function fetchFile(File $file, string $propertyName, ObjectEntity $objectEntity): File + { + $fileContent = null; + + // Encode special characters in the URL + $encodedUrl = rawurlencode($file->getAccessUrl()); + + + // Decode valid path separators and reserved characters + $encodedUrl = str_replace(['%2F', '%3A', '%28', '%29'], ['/', ':', '(', ')'], $encodedUrl); + + if (filter_var($encodedUrl, FILTER_VALIDATE_URL)) { + $this->setExtension($file); + try { + + if ($file->getSource() !== null) { + $sourceMapper = $this->getOpenConnector(filePath: '\Db\SourceMapper'); + $source = $sourceMapper->find($file->getSource()); + + $callService = $this->getOpenConnector(filePath: '\Service\CallService'); + if ($callService === null) { + throw new Exception("OpenConnector service not available"); + } + $endpoint = str_replace($source->getLocation(), "", $encodedUrl); + $endpoint = urldecode($endpoint); + $response = $callService->call(source: $source, endpoint: $endpoint, method: 'GET')->getResponse(); + + $fileContent = $response['body']; + + if( + $response['encoding'] === 'base64' + ) { + $fileContent = base64_decode(string: $fileContent); + } + + } else { + $client = new Client(); + $response = $client->get($encodedUrl); + $fileContent = $response->getBody()->getContents(); + } + } catch (Exception|NotFoundExceptionInterface $e) { + throw new Exception('Failed to download file from URL: ' . $e->getMessage()); + } + } + + $this->writeFile(fileContent: $fileContent, propertyName: $propertyName, objectEntity: $objectEntity, file: $file); + + return $file; + } + + /** + * Handle file property processing + * + * @param ObjectEntity $objectEntity The object entity + * @param array $object The object data + * @param string $propertyName The name of the file property + * + * @return array Updated object data + * @throws Exception|GuzzleException When file handling fails + */ + private function handleFileProperty(ObjectEntity $objectEntity, array $object, string $propertyName, ?string $format = null): array + { + $fileName = str_replace('.', '_', $propertyName); + $objectDot = new Dot($object); + + // Handle base64 encoded file + if (is_string($objectDot->get("$propertyName.base64")) === true + && preg_match('/^data:([^;]*);base64,(.*)/', $objectDot->get("$propertyName.base64"), $matches) + ) { + unset($object[$propertyName]['base64']); + $fileEntity = new File(); + $fileEntity->hydrate($object[$propertyName]); + $fileEntity->setFilename($fileName); + $this->setExtension($fileEntity); + + $this->fileMapper->insert($fileEntity); + + $fileContent = base64_decode($matches[2], true); + if ($fileContent === false) { + throw new Exception('Invalid base64 encoded file'); + } + + $fileEntity = $this->writeFile(fileContent: $fileContent, propertyName: $propertyName, objectEntity: $objectEntity, file: $fileEntity); + } + // Handle URL file + else { + $fileEntities = $this->fileMapper->findAll(filters: ['accessUrl' => $objectDot->get("$propertyName.accessUrl")]); + if(count($fileEntities) > 0) { + $fileEntity = $fileEntities[0]; + } + + if (count($fileEntities) === 0) { + $fileEntity = new File(); + $fileEntity->hydrate($object[$propertyName]); + } + + if($fileEntity->getFilename() === null) { + $fileEntity->setFilename($fileName); + } + + if($fileEntity->getChecksum() === null || $fileEntity->getUpdated() > new DateTime('-5 minutes')) { + $fileEntity = $this->fetchFile(file: $fileEntity, propertyName: $propertyName, objectEntity: $objectEntity); + $fileEntity->setUpdated(new DateTime()); + } + } + + $fileEntity->setChecksum(md5(serialize($fileContent))); + + $this->fileMapper->update($fileEntity); + + switch($format) { + case 'filename': + return $fileEntity->getFileName(); + case 'extension': + return $fileEntity->getExtension(); + case 'shareUrl': + return $fileEntity->getShareUrl(); + case 'accessUrl': + return $fileEntity->getAccessUrl(); + case 'downloadUrl': + default: + return $fileEntity->getDownloadUrl(); + } } /** From 6957ef5bcab004bf54caf99ebe516f95baca87a9 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Mon, 16 Dec 2024 16:51:52 +0100 Subject: [PATCH 6/6] Fixes from testing --- lib/Db/File.php | 8 +-- lib/Db/FileMapper.php | 45 +++++++++++-- lib/Migration/Version1Date20241216094112.php | 70 ++++++++++++++++++++ lib/Service/ObjectService.php | 17 +++-- 4 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 lib/Migration/Version1Date20241216094112.php diff --git a/lib/Db/File.php b/lib/Db/File.php index 790ce6d6..c4c946dc 100644 --- a/lib/Db/File.php +++ b/lib/Db/File.php @@ -9,19 +9,19 @@ class File extends Entity implements JsonSerializable { - protected string $uuid; + protected ?string $uuid = null; protected ?string $filename = null; protected ?string $downloadUrl = null; protected ?string $shareUrl = null; protected ?string $accessUrl = null; protected ?string $extension = null; protected ?string $checksum = null; - protected ?string $source = null; + protected ?int $source = null; protected ?string $userId = null; protected ?string $base64 = null; protected ?string $filePath = null; - protected DateTime $created; - protected DateTime $updated; + protected ?DateTime $created = null; + protected ?DateTime $updated = null; public function __construct() { $this->addType('uuid', 'string'); diff --git a/lib/Db/FileMapper.php b/lib/Db/FileMapper.php index e3e29ff9..e495d85e 100644 --- a/lib/Db/FileMapper.php +++ b/lib/Db/FileMapper.php @@ -2,6 +2,7 @@ namespace OCA\OpenRegister\Db; +use DateTime; use OCA\OpenRegister\Db\File; use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\QBMapper; @@ -13,7 +14,7 @@ class FileMapper extends QBMapper { public function __construct(IDBConnection $db) { - parent::__construct($db, 'openconnector_jobs'); + parent::__construct($db, 'openregister_files'); } public function find(int $id): File @@ -21,7 +22,7 @@ public function find(int $id): File $qb = $this->db->getQueryBuilder(); $qb->select('*') - ->from('openconnector_jobs') + ->from('openregister_files') ->where( $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) ); @@ -34,11 +35,12 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters $qb = $this->db->getQueryBuilder(); $qb->select('*') - ->from('openconnector_jobs') + ->from('openregister_files') ->setMaxResults($limit) ->setFirstResult($offset); foreach ($filters as $filter => $value) { + $filter = strtolower(preg_replace('/(?andWhere($qb->expr()->isNotNull($filter)); } elseif ($value === 'IS NULL') { @@ -58,6 +60,41 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters return $this->findEntities(query: $qb); } + /** + * @inheritDoc + * + * @param \OCA\OpenRegister\Db\File|Entity $entity + * @return \OCA\OpenRegister\Db\File + * @throws \OCP\DB\Exception + */ + public function insert(File|Entity $entity): File + { + // Set created and updated fields + $entity->setCreated(new DateTime()); + $entity->setUpdated(new DateTime()); + + if($entity->getUuid() === null) { + $entity->setUuid(Uuid::v4()); + } + + return parent::insert($entity); + } + + /** + * @inheritDoc + * + * @param \OCA\OpenRegister\Db\File|Entity $entity + * @return \OCA\OpenRegister\Db\File + * @throws \OCP\DB\Exception + */ + public function update(File|Entity $entity): File + { + // Set updated field + $entity->setUpdated(new DateTime()); + + return parent::update($entity); + } + public function createFromArray(array $object): File { $obj = new File(); @@ -93,7 +130,7 @@ public function getTotalCallCount(): int // Select count of all logs $qb->select($qb->createFunction('COUNT(*) as count')) - ->from('openconnector_jobs'); + ->from('openregister_files'); $result = $qb->execute(); $row = $result->fetch(); diff --git a/lib/Migration/Version1Date20241216094112.php b/lib/Migration/Version1Date20241216094112.php new file mode 100644 index 00000000..341dc4bf --- /dev/null +++ b/lib/Migration/Version1Date20241216094112.php @@ -0,0 +1,70 @@ +hasTable('openregister_files') === false) { + $table = $schema->createTable('openregister_files'); + $table->addColumn(name: 'id', typeName: Types::BIGINT, options: ['autoincrement' => true, 'notnull' => true, 'length' => 255]); + $table->addColumn(name: 'uuid', typeName: Types::STRING, options: ['notnull' => true, 'length' => 255]); + $table->addColumn(name: 'filename', typeName: Types::STRING, options: ['notnull' => false, 'length' => 255]); + $table->addColumn(name: 'download_url', typeName: Types::STRING, options: ['notnull' => false, 'length' => 1023]); + $table->addColumn(name: 'share_url', typeName: Types::STRING, options: ['notnull' => false, 'length' => 1023]); + $table->addColumn(name: 'access_url', typeName: Types::STRING, options: ['notnull' => false, 'length' => 1023]); + $table->addColumn(name: 'extension', typeName: Types::STRING, options: ['notnull' => false, 'length' => 255]); + $table->addColumn(name: 'checksum', typeName: Types::STRING, options: ['notnull' => false, 'length' => 255]); + $table->addColumn(name: 'source', typeName: Types::INTEGER, options: ['notnull' => false, 'length' => 255]); + $table->addColumn(name: 'user_id', typeName: Types::STRING, options: ['notnull' => false, 'length' => 255]); + $table->addColumn(name: 'created', typeName: Types::DATETIME_IMMUTABLE, options: ['notnull' => true, 'length' => 255]); + $table->addColumn(name: 'updated', typeName: Types::DATETIME_MUTABLE, options: ['notnull' => true, 'length' => 255]); + $table->addColumn(name: 'file_path', typeName: Types::STRING); + + $table->setPrimaryKey(['id']); + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } +} diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 25f0b22c..f73b8b6a 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -865,7 +865,7 @@ private function handleObjectRelations(ObjectEntity $objectEntity, array $object private function writeFile(string $fileContent, string $propertyName, ObjectEntity $objectEntity, File $file): File { - $fileName = $file->getFileName(); + $fileName = $file->getFilename(); try { $schema = $this->schemaMapper->find($objectEntity->getSchema()); @@ -875,7 +875,13 @@ private function writeFile(string $fileContent, string $propertyName, ObjectEnti $this->fileService->createFolder(folderPath: 'Objects'); $this->fileService->createFolder(folderPath: "Objects/$schemaFolder"); $this->fileService->createFolder(folderPath: "Objects/$schemaFolder/$objectFolder"); - $filePath = "Objects/$schemaFolder/$objectFolder/$fileName"; + + $filePath = $file->getFilePath(); + + if($filePath === null) { + $filePath = "Objects/$schemaFolder/$objectFolder/$fileName"; + } + $succes = $this->fileService->updateFile( content: $fileContent, @@ -903,6 +909,7 @@ private function writeFile(string $fileContent, string $propertyName, ObjectEnti // Preserve the original uri in the object 'json blob' $file->setDownloadUrl($downloadLink); $file->setShareUrl($shareLink); + $file->setFilePath($filePath); } catch (Exception $e) { throw new Exception('Failed to store file: ' . $e->getMessage()); } @@ -965,6 +972,7 @@ private function fetchFile(File $file, string $propertyName, ObjectEntity $objec } } + $this->writeFile(fileContent: $fileContent, propertyName: $propertyName, objectEntity: $objectEntity, file: $file); return $file; @@ -980,7 +988,7 @@ private function fetchFile(File $file, string $propertyName, ObjectEntity $objec * @return array Updated object data * @throws Exception|GuzzleException When file handling fails */ - private function handleFileProperty(ObjectEntity $objectEntity, array $object, string $propertyName, ?string $format = null): array + private function handleFileProperty(ObjectEntity $objectEntity, array $object, string $propertyName, ?string $format = null): string { $fileName = str_replace('.', '_', $propertyName); $objectDot = new Dot($object); @@ -1012,8 +1020,7 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s } if (count($fileEntities) === 0) { - $fileEntity = new File(); - $fileEntity->hydrate($object[$propertyName]); + $fileEntity = $this->fileMapper->createFromArray($object[$propertyName]); } if($fileEntity->getFilename() === null) {