diff --git a/apps/files/lib/Controller/ResumableUploadController.php b/apps/files/lib/Controller/ResumableUploadController.php index a0fdea435ad..c4b6ed6e6b5 100644 --- a/apps/files/lib/Controller/ResumableUploadController.php +++ b/apps/files/lib/Controller/ResumableUploadController.php @@ -20,12 +20,15 @@ use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\Response; +use OCP\Files\IMimeTypeDetector; +use OCP\Files\IRootFolder; use OCP\IRequest; use OCP\IURLGenerator; +use OCP\Server; /** * Implementation of https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-resumable-upload-05 - * All functionality described by the draft RFC is excluded from OpenAPI. + * All functionality described by the draft RFC is excluded from OpenAPI, only the custom endpoint to finish the upload is included. */ class ResumableUploadController extends Controller { // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-resumable-upload-05#section-4.2-2 @@ -385,4 +388,73 @@ class ResumableUploadController extends Controller { // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-resumable-upload-05#section-7-4 return new Response(Http::STATUS_NO_CONTENT, self::BASE_HEADERS); } + + /** + * Finish the upload. + * + * @param string $token The token of the upload + * @param string $path The final path where the file will be moved to + * @param int $createdTimestamp The unix timestamp of when the file was created + * @param int $lastModifiedTimestamp The unix timestamp of when the file was last modified + * @param bool $overwrite Whether an existing file should be overwritten + * @return Response + * + * 204: Upload finished successfully + * 400: Upload not complete + * 401: User is unauthorized + * 404: Upload not found + * 409: File already exists + */ + #[NoAdminRequired] + #[NoCSRFRequired] + #[FrontpageRoute(verb: 'POST', url: '/upload/{token}/finish')] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] + public function finishUpload( + string $token, + string $path, + int $createdTimestamp, + int $lastModifiedTimestamp, + bool $overwrite = false, + ): Response { + if ($this->userId === null) { + return new Response(Http::STATUS_UNAUTHORIZED); // @codeCoverageIgnore + } + + $upload = $this->mapper->findByToken($this->userId, $token); + if (!$upload instanceof ResumableUpload) { + return new Response(Http::STATUS_NOT_FOUND); + } + + if (!$upload->getComplete()) { + return new Response(Http::STATUS_BAD_REQUEST); + } + + $userFolder = Server::get(IRootFolder::class)->getUserFolder($this->userId); + + if ($userFolder->nodeExists($path)) { + if (!$overwrite) { + return new Response(Http::STATUS_CONFLICT); + } + + $userFolder->get($path)->delete(); + } + + $tmpFileHandle = fopen($upload->getPath(), 'rb'); + $outFile = $userFolder->newFile($path); + + $outFile->putContent($tmpFileHandle); + + $userFolder->getStorage()->getCache()->put($outFile->getInternalPath(), [ + 'creation_time' => $createdTimestamp, + 'upload_time' => time(), + 'mtime' => $lastModifiedTimestamp, + // TODO: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-resumable-upload-05#name-upload-metadata + 'mimetype' => Server::get(IMimeTypeDetector::class)->detectPath($path), + ]); + + unlink($upload->getPath()); + $this->mapper->delete($upload); + + return new Response(Http::STATUS_NO_CONTENT); + } } diff --git a/apps/files/openapi.json b/apps/files/openapi.json index 5c78b30ffa6..851b7b04b80 100644 --- a/apps/files/openapi.json +++ b/apps/files/openapi.json @@ -569,6 +569,105 @@ } } }, + "/index.php/apps/files/upload/{token}/finish": { + "post": { + "operationId": "resumable_upload-finish-upload", + "summary": "Finish the upload.", + "tags": [ + "resumable_upload" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "path", + "createdTimestamp", + "lastModifiedTimestamp" + ], + "properties": { + "path": { + "type": "string", + "description": "The final path where the file will be moved to" + }, + "createdTimestamp": { + "type": "integer", + "format": "int64", + "description": "The unix timestamp of when the file was created" + }, + "lastModifiedTimestamp": { + "type": "integer", + "format": "int64", + "description": "The unix timestamp of when the file was last modified" + }, + "overwrite": { + "type": "boolean", + "default": false, + "description": "Whether an existing file should be overwritten" + } + } + } + } + } + }, + "parameters": [ + { + "name": "token", + "in": "path", + "description": "The token of the upload", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Upload finished successfully" + }, + "400": { + "description": "Upload not complete" + }, + "401": { + "description": "User is unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + }, + "404": { + "description": "Upload not found" + }, + "409": { + "description": "File already exists" + }, + "500": { + "description": "" + } + } + } + }, "/ocs/v2.php/apps/files/api/v1/directEditing": { "get": { "operationId": "direct_editing-info", @@ -2907,7 +3006,7 @@ "tags": [ { "name": "resumable_upload", - "description": "Implementation of https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-resumable-upload-05 All functionality described by the draft RFC is excluded from OpenAPI." + "description": "Implementation of https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-resumable-upload-05 All functionality described by the draft RFC is excluded from OpenAPI, only the custom endpoint to finish the upload is included." } ] } diff --git a/apps/files/tests/Controller/ResumableUploadControllerTest.php b/apps/files/tests/Controller/ResumableUploadControllerTest.php index 9b1a783e78d..a7df0f439f3 100644 --- a/apps/files/tests/Controller/ResumableUploadControllerTest.php +++ b/apps/files/tests/Controller/ResumableUploadControllerTest.php @@ -13,15 +13,29 @@ use OCA\Files\Db\ResumableUploadMapper; use OCA\Files\Response\AProblemResponse; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Response; +use OCP\Files\File; +use OCP\Files\IRootFolder; use OCP\IRequest; use OCP\IURLGenerator; use OCP\Server; use Test\TestCase; +use Test\Traits\UserTrait; /** * @group DB */ class ResumableUploadControllerTest extends TestCase { + use UserTrait; + + private const username = 'user'; + + public function setUp(): void { + parent::setUp(); + + $this->createUser(self::username, ''); + self::loginAsUser(self::username); + } + /** * @psalm-param Callable(ResumableUploadController):Response $method * @param array $requestHeaders @@ -50,7 +64,7 @@ class ResumableUploadControllerTest extends TestCase { $controller = new ResumableUploadController( 'files', $request, - 'user', + self::username, Server::get(IURLGenerator::class), Server::get(ResumableUploadMapper::class), $inputHandle, @@ -79,6 +93,7 @@ class ResumableUploadControllerTest extends TestCase { unset( $responseHeaders[ResumableUploadController::HTTP_HEADER_LOCATION], $headers[ResumableUploadController::HTTP_HEADER_LOCATION], + $headers['X-User-Id'], ); $this->assertEquals($responseStatusCode, $response->getStatus()); @@ -1131,4 +1146,306 @@ class ResumableUploadControllerTest extends TestCase { fn (ResumableUploadController $controller): Response => $controller->deleteResource('404'), ); } + + public function testFinish(): void { + $response = $this->performRequest( + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ResumableUploadController::HTTP_HEADER_UPLOAD_COMPLETE => '0', + ResumableUploadController::HTTP_HEADER_UPLOAD_LENGTH => '9', + ], + 'abc', + Http::STATUS_CREATED, + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ResumableUploadController::HTTP_HEADER_UPLOAD_COMPLETE => '0', + ResumableUploadController::HTTP_HEADER_UPLOAD_OFFSET => 3, + ResumableUploadController::HTTP_HEADER_LOCATION => true, + ], + '', + fn (ResumableUploadController $controller): Response => $controller->createResource(), + ); + $token = $this->getTokenFromResponse($response); + $this->performRequest( + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ], + '', + Http::STATUS_NO_CONTENT, + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ResumableUploadController::HTTP_HEADER_UPLOAD_COMPLETE => '0', + ResumableUploadController::HTTP_HEADER_UPLOAD_OFFSET => 3, + ResumableUploadController::HTTP_HEADER_UPLOAD_LENGTH => 9, + ResumableUploadController::HTTP_HEADER_CACHE_CONTROL => 'no-store', + ], + '', + fn (ResumableUploadController $controller): Response => $controller->checkResource($token), + ); + + $this->performRequest( + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ResumableUploadController::HTTP_HEADER_UPLOAD_OFFSET => '3', + ResumableUploadController::HTTP_HEADER_CONTENT_TYPE => ResumableUploadController::MEDIA_TYPE_PARTIAL_UPLOAD, + ], + 'def', + Http::STATUS_CREATED, + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ResumableUploadController::HTTP_HEADER_UPLOAD_COMPLETE => '0', + ResumableUploadController::HTTP_HEADER_UPLOAD_OFFSET => 6, + ], + '', + fn (ResumableUploadController $controller): Response => $controller->appendResource($token), + ); + $this->performRequest( + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ], + '', + Http::STATUS_NO_CONTENT, + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ResumableUploadController::HTTP_HEADER_UPLOAD_COMPLETE => '0', + ResumableUploadController::HTTP_HEADER_UPLOAD_OFFSET => 6, + ResumableUploadController::HTTP_HEADER_UPLOAD_LENGTH => 9, + ResumableUploadController::HTTP_HEADER_CACHE_CONTROL => 'no-store', + ], + '', + fn (ResumableUploadController $controller): Response => $controller->checkResource($token), + ); + + $this->performRequest( + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ResumableUploadController::HTTP_HEADER_UPLOAD_COMPLETE => '1', + ResumableUploadController::HTTP_HEADER_UPLOAD_OFFSET => '6', + ResumableUploadController::HTTP_HEADER_CONTENT_TYPE => ResumableUploadController::MEDIA_TYPE_PARTIAL_UPLOAD, + ], + 'ghi', + Http::STATUS_CREATED, + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ResumableUploadController::HTTP_HEADER_UPLOAD_COMPLETE => '1', + ResumableUploadController::HTTP_HEADER_UPLOAD_OFFSET => 9, + ], + '', + fn (ResumableUploadController $controller): Response => $controller->appendResource($token), + ); + $this->performRequest( + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ], + '', + Http::STATUS_NO_CONTENT, + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ResumableUploadController::HTTP_HEADER_UPLOAD_COMPLETE => '1', + ResumableUploadController::HTTP_HEADER_UPLOAD_OFFSET => 9, + ResumableUploadController::HTTP_HEADER_UPLOAD_LENGTH => 9, + ResumableUploadController::HTTP_HEADER_CACHE_CONTROL => 'no-store', + ], + '', + fn (ResumableUploadController $controller): Response => $controller->checkResource($token), + ); + + $start = time(); + $this->performRequest( + [], + '', + Http::STATUS_NO_CONTENT, + [], + '', + fn (ResumableUploadController $controller): Response => $controller->finishUpload($token, '/test.txt', 123, 456), + ); + $end = time(); + + $userFolder = Server::get(IRootFolder::class)->getUserFolder('user'); + + /** @var File $file */ + $file = $userFolder->get('test.txt'); + $this->assertEquals('abcdefghi', $file->getContent()); + $cacheEntry = $userFolder->getStorage()->getCache()->get($file->getInternalPath()); + $this->assertNotFalse($cacheEntry); + $this->assertEquals('files/test.txt', $cacheEntry->getPath()); + $this->assertEquals(9, $cacheEntry->getSize()); + $this->assertEquals(123, $cacheEntry->getCreationTime()); + $this->assertEquals(456, $cacheEntry->getMTime()); + $this->assertGreaterThanOrEqual($start, $cacheEntry->getUploadTime()); + $this->assertLessThanOrEqual($end, $cacheEntry->getUploadTime()); + $this->assertEquals('text/plain', $cacheEntry->getMimetype()); + } + + public function testFinishIncomplete(): void { + $response = $this->performRequest( + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ResumableUploadController::HTTP_HEADER_UPLOAD_COMPLETE => '0', + ResumableUploadController::HTTP_HEADER_UPLOAD_LENGTH => '9', + ], + 'abc', + Http::STATUS_CREATED, + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ResumableUploadController::HTTP_HEADER_UPLOAD_COMPLETE => '0', + ResumableUploadController::HTTP_HEADER_UPLOAD_OFFSET => 3, + ResumableUploadController::HTTP_HEADER_LOCATION => true, + ], + '', + fn (ResumableUploadController $controller): Response => $controller->createResource(), + ); + $token = $this->getTokenFromResponse($response); + $this->performRequest( + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ], + '', + Http::STATUS_NO_CONTENT, + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ResumableUploadController::HTTP_HEADER_UPLOAD_COMPLETE => '0', + ResumableUploadController::HTTP_HEADER_UPLOAD_OFFSET => 3, + ResumableUploadController::HTTP_HEADER_UPLOAD_LENGTH => 9, + ResumableUploadController::HTTP_HEADER_CACHE_CONTROL => 'no-store', + ], + '', + fn (ResumableUploadController $controller): Response => $controller->checkResource($token), + ); + + $this->performRequest( + [], + '', + Http::STATUS_BAD_REQUEST, + [], + '', + fn (ResumableUploadController $controller): Response => $controller->finishUpload($token, '/test.txt', 123, 456), + ); + } + + public function testFinishExistingFile(): void { + $userFolder = Server::get(IRootFolder::class)->getUserFolder('user'); + + $this->assertEquals(1, $userFolder->newFile('test.txt', 'z')->getSize()); + + $response = $this->performRequest( + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ResumableUploadController::HTTP_HEADER_UPLOAD_COMPLETE => '1', + ResumableUploadController::HTTP_HEADER_UPLOAD_LENGTH => '3', + ], + 'abc', + Http::STATUS_CREATED, + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ResumableUploadController::HTTP_HEADER_UPLOAD_COMPLETE => '1', + ResumableUploadController::HTTP_HEADER_UPLOAD_OFFSET => 3, + ResumableUploadController::HTTP_HEADER_LOCATION => true, + ], + '', + fn (ResumableUploadController $controller): Response => $controller->createResource(), + ); + $token = $this->getTokenFromResponse($response); + $this->performRequest( + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ], + '', + Http::STATUS_NO_CONTENT, + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ResumableUploadController::HTTP_HEADER_UPLOAD_COMPLETE => '1', + ResumableUploadController::HTTP_HEADER_UPLOAD_OFFSET => 3, + ResumableUploadController::HTTP_HEADER_UPLOAD_LENGTH => 3, + ResumableUploadController::HTTP_HEADER_CACHE_CONTROL => 'no-store', + ], + '', + fn (ResumableUploadController $controller): Response => $controller->checkResource($token), + ); + + $this->performRequest( + [], + '', + Http::STATUS_CONFLICT, + [], + '', + fn (ResumableUploadController $controller): Response => $controller->finishUpload($token, '/test.txt', 123, 456), + ); + } + + public function testFinishExistingFileOverwrite(): void { + $userFolder = Server::get(IRootFolder::class)->getUserFolder('user'); + + $this->assertEquals(1, $userFolder->newFile('test.txt', 'z')->getSize()); + + $response = $this->performRequest( + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ResumableUploadController::HTTP_HEADER_UPLOAD_COMPLETE => '1', + ResumableUploadController::HTTP_HEADER_UPLOAD_LENGTH => '3', + ], + 'abc', + Http::STATUS_CREATED, + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ResumableUploadController::HTTP_HEADER_UPLOAD_COMPLETE => '1', + ResumableUploadController::HTTP_HEADER_UPLOAD_OFFSET => 3, + ResumableUploadController::HTTP_HEADER_LOCATION => true, + ], + '', + fn (ResumableUploadController $controller): Response => $controller->createResource(), + ); + $token = $this->getTokenFromResponse($response); + $this->performRequest( + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ], + '', + Http::STATUS_NO_CONTENT, + [ + ResumableUploadController::HTTP_HEADER_UPLOAD_DRAFT_INTEROP_VERSION => ResumableUploadController::UPLOAD_DRAFT_INTEROP_VERSION, + ResumableUploadController::HTTP_HEADER_UPLOAD_COMPLETE => '1', + ResumableUploadController::HTTP_HEADER_UPLOAD_OFFSET => 3, + ResumableUploadController::HTTP_HEADER_UPLOAD_LENGTH => 3, + ResumableUploadController::HTTP_HEADER_CACHE_CONTROL => 'no-store', + ], + '', + fn (ResumableUploadController $controller): Response => $controller->checkResource($token), + ); + + $start = time(); + $this->performRequest( + [], + '', + Http::STATUS_NO_CONTENT, + [], + '', + fn (ResumableUploadController $controller): Response => $controller->finishUpload($token, '/test.txt', 123, 456, true), + ); + $end = time(); + + /** @var File $file */ + $file = $userFolder->get('test.txt'); + $this->assertEquals('abc', $file->getContent()); + $cacheEntry = $userFolder->getStorage()->getCache()->get($file->getInternalPath()); + $this->assertNotFalse($cacheEntry); + $this->assertEquals('files/test.txt', $cacheEntry->getPath()); + $this->assertEquals(3, $cacheEntry->getSize()); + $this->assertEquals(123, $cacheEntry->getCreationTime()); + $this->assertEquals(456, $cacheEntry->getMTime()); + $this->assertGreaterThanOrEqual($start, $cacheEntry->getUploadTime()); + $this->assertLessThanOrEqual($end, $cacheEntry->getUploadTime()); + $this->assertEquals('text/plain', $cacheEntry->getMimetype()); + } + + public function testFinishNonExistent(): void { + $this->performRequest( + [], + '', + Http::STATUS_NOT_FOUND, + [], + '', + fn (ResumableUploadController $controller): Response => $controller->finishUpload('404', '/test.txt', 123, 456), + ); + } } diff --git a/openapi.json b/openapi.json index e7e77387080..8cac2eab208 100644 --- a/openapi.json +++ b/openapi.json @@ -51,7 +51,7 @@ }, { "name": "files/resumable_upload", - "description": "Implementation of https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-resumable-upload-05 All functionality described by the draft RFC is excluded from OpenAPI." + "description": "Implementation of https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-resumable-upload-05 All functionality described by the draft RFC is excluded from OpenAPI, only the custom endpoint to finish the upload is included." }, { "name": "theming/theming", @@ -20980,6 +20980,105 @@ } } }, + "/index.php/apps/files/upload/{token}/finish": { + "post": { + "operationId": "files-resumable_upload-finish-upload", + "summary": "Finish the upload.", + "tags": [ + "files/resumable_upload" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "path", + "createdTimestamp", + "lastModifiedTimestamp" + ], + "properties": { + "path": { + "type": "string", + "description": "The final path where the file will be moved to" + }, + "createdTimestamp": { + "type": "integer", + "format": "int64", + "description": "The unix timestamp of when the file was created" + }, + "lastModifiedTimestamp": { + "type": "integer", + "format": "int64", + "description": "The unix timestamp of when the file was last modified" + }, + "overwrite": { + "type": "boolean", + "default": false, + "description": "Whether an existing file should be overwritten" + } + } + } + } + } + }, + "parameters": [ + { + "name": "token", + "in": "path", + "description": "The token of the upload", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Upload finished successfully" + }, + "400": { + "description": "Upload not complete" + }, + "401": { + "description": "User is unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + }, + "404": { + "description": "Upload not found" + }, + "409": { + "description": "File already exists" + }, + "500": { + "description": "" + } + } + } + }, "/ocs/v2.php/apps/files/api/v1/directEditing": { "get": { "operationId": "files-direct_editing-info",