Skip to content
Open
Changes from all commits
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
44 changes: 39 additions & 5 deletions apps/dav/lib/Upload/ChunkingV2Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class ChunkingV2Plugin extends ServerPlugin {

private ?string $uploadId = null;
private ?string $uploadPath = null;
private ?int $uploadTargetId = null;

private const TEMP_TARGET = '.target';

Expand All @@ -61,6 +62,17 @@ class ChunkingV2Plugin extends ServerPlugin {

private const DESTINATION_HEADER = 'Destination';

/**
* Lifetime of the chunked upload session metadata in the distributed cache.
*
* The TTL is refreshed on every successful chunk upload (sliding
* expiration), so it acts as an inactivity timeout rather than a hard
* wall-clock limit. Without the refresh an upload whose total duration
* exceeds this value would fail with "Missing metadata for chunked upload"
* even while chunks are still actively being uploaded.
*/
private const UPLOAD_SESSION_TTL = 24 * 60 * 60;

public function __construct(ICacheFactory $cacheFactory) {
$this->cache = $cacheFactory->createDistributed(self::CACHE_KEY);
}
Expand Down Expand Up @@ -130,12 +142,9 @@ public function afterMkcol(RequestInterface $request, ResponseInterface $respons
[$storage, $storagePath] = $this->getUploadStorage($this->uploadPath);

$this->uploadId = $storage->startChunkedWrite($storagePath);
$this->uploadTargetId = $targetFile->getId();

$this->cache->set($this->uploadFolder->getName(), [
self::UPLOAD_ID => $this->uploadId,
self::UPLOAD_TARGET_PATH => $this->uploadPath,
self::UPLOAD_TARGET_ID => $targetFile->getId(),
], 86400);
$this->storeUploadSession();

$response->setStatus(Http::STATUS_CREATED);
return true;
Expand Down Expand Up @@ -177,6 +186,12 @@ public function beforePut(RequestInterface $request, ResponseInterface $response
$stream = $request->getBodyAsStream();
$storage->putChunkedWritePart($storagePath, $this->uploadId, (string)$partId, $stream, $additionalSize);

// Refresh the session metadata TTL on every successful chunk so an
// actively progressing upload is not garbage-collected purely on
// wall-clock age (sliding expiration). See afterMkcol() for the
// initial write.
$this->storeUploadSession();

$storage->getCache()->update($uploadFile->getId(), ['size' => $uploadFile->getSize() + $additionalSize]);
if ($tempTargetFile) {
$storage->getPropagator()->propagateChange($tempTargetFile->getInternalPath(), time(), $additionalSize);
Expand Down Expand Up @@ -317,6 +332,25 @@ public function prepareUpload($path): void {
$uploadMetadata = $this->cache->get($this->uploadFolder->getName());
$this->uploadId = $uploadMetadata[self::UPLOAD_ID] ?? null;
$this->uploadPath = $uploadMetadata[self::UPLOAD_TARGET_PATH] ?? null;
$this->uploadTargetId = $uploadMetadata[self::UPLOAD_TARGET_ID] ?? null;
}

/**
* Persist the chunked upload session metadata in the distributed cache and
* (re)set its TTL. Called once when the session is created in afterMkcol()
* and again after every successful chunk in beforePut() to provide a
* sliding expiration based on activity rather than a fixed lifetime.
*/
private function storeUploadSession(): void {
if ($this->uploadId === null || $this->uploadPath === null || $this->uploadTargetId === null) {
return;
}

$this->cache->set($this->uploadFolder->getName(), [
self::UPLOAD_ID => $this->uploadId,
self::UPLOAD_TARGET_PATH => $this->uploadPath,
self::UPLOAD_TARGET_ID => $this->uploadTargetId,
], self::UPLOAD_SESSION_TTL);
}

private function completeChunkedWrite(string $targetAbsolutePath): void {
Expand Down
Loading