Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(files): Implement resumable upload draft RFC #49753

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(files): Add custom endpoint for finishing resumable upload
Signed-off-by: provokateurin <kate@provokateurin.de>
  • Loading branch information
provokateurin committed Dec 10, 2024
commit 59826c2cf7908e75f7481d49bd3b2c692d4a08f0
82 changes: 81 additions & 1 deletion apps/files/lib/Controller/ResumableUploadController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace OCA\Files\Controller;

use OC\Files\Filesystem;
use OCA\Files\Db\ResumableUpload;
use OCA\Files\Db\ResumableUploadMapper;
use OCA\Files\Response\CompleteUploadResponse;
Expand All @@ -20,12 +21,14 @@
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Response;
use OCP\Files\IMimeTypeDetector;
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
Expand Down Expand Up @@ -366,4 +369,81 @@ public function deleteResource(string $token): Response {
// 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')]
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 === null) {
return new Response(Http::STATUS_NOT_FOUND);
}

if (!$upload->getComplete()) {
return new Response(Http::STATUS_BAD_REQUEST);
}

$view = Filesystem::getView();
if ($view === null) {
return new Response(Http::STATUS_INTERNAL_SERVER_ERROR); // @codeCoverageIgnore
}

if ($view->file_exists($path)) {
if (!$overwrite) {
return new Response(Http::STATUS_CONFLICT);
}

$view->unlink($path);
}

$tmpFileHandle = fopen($upload->getPath(), 'rb');
$outFileHandle = $view->fopen($path, 'wb');

$copied = stream_copy_to_stream($tmpFileHandle, $outFileHandle);
if ($copied === false) {
return new Response(Http::STATUS_INTERNAL_SERVER_ERROR); // @codeCoverageIgnore
}

fclose($tmpFileHandle);
fclose($outFileHandle);

$view->putFileInfo($path, [
'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);
}
}
86 changes: 85 additions & 1 deletion apps/files/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,90 @@
}
}
},
"/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"
},
"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",
Expand Down Expand Up @@ -2242,7 +2326,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."
}
]
}
Loading