mirror of
https://github.com/nextcloud/server.git
synced 2026-02-03 20:41:22 -05:00
feat(files): Add custom endpoint for finishing resumable upload
Signed-off-by: provokateurin <kate@provokateurin.de>
This commit is contained in:
parent
01e3edb572
commit
de502ca3d0
4 changed files with 591 additions and 4 deletions
|
|
@ -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<Http::STATUS_NO_CONTENT|Http::STATUS_BAD_REQUEST|Http::STATUS_UNAUTHORIZED|Http::STATUS_NOT_FOUND|Http::STATUS_CONFLICT|Http::STATUS_INTERNAL_SERVER_ERROR, array{}>
|
||||
*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, string> $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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
101
openapi.json
101
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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue