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

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

private const string TEMP_TARGET = '.target';

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

private const string 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 @@ -155,12 +167,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 @@ -202,6 +211,9 @@ 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
$this->storeUploadSession();

$storage->getCache()->update($uploadFile->getId(), ['size' => $uploadFile->getSize() + $additionalSize]);
if ($tempTargetFile) {
$storage->getPropagator()->propagateChange($tempTargetFile->getInternalPath(), time(), $additionalSize);
Expand Down Expand Up @@ -342,6 +354,23 @@ 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 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