From 13467cbdbc764e066cb1578bca463d18c24bb27c Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Wed, 13 Aug 2025 10:27:55 +0200 Subject: [PATCH 1/2] fix(Streamer): use localtime for ZIP files ZIP does not use a proper timestamp but uses something called "DOS time". This is a weird old format with some limitations like accuracy of only 2 seconds, but also no timezone information. Also unline UNIX time it is not relative to some specific point in time with timezone information, but is always considered to be the local time. Meaning we need to convert it first to the users local time. Signed-off-by: Ferdinand Thiessen --- lib/private/Streamer.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/private/Streamer.php b/lib/private/Streamer.php index e579c42c0d7ff..ca03295b02678 100644 --- a/lib/private/Streamer.php +++ b/lib/private/Streamer.php @@ -14,6 +14,7 @@ use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; +use OCP\IDateTimeZone; use OCP\IRequest; use ownCloud\TarStreamer\TarStreamer; use Psr\Log\LoggerInterface; @@ -156,7 +157,7 @@ public function addFileFromStream($stream, string $internalName, int|float $size $options = []; if ($time) { $options = [ - 'timestamp' => $time + 'timestamp' => $this->fixTimestamp($time), ]; } @@ -176,7 +177,7 @@ public function addFileFromStream($stream, string $internalName, int|float $size public function addEmptyDir(string $dirName, int $timestamp = 0): bool { $options = null; if ($timestamp > 0) { - $options = ['timestamp' => $timestamp]; + $options = ['timestamp' => $this->fixTimestamp($timestamp)]; } return $this->streamerInstance->addEmptyDir($dirName, $options); @@ -191,4 +192,14 @@ public function addEmptyDir(string $dirName, int $timestamp = 0): bool { public function finalize() { return $this->streamerInstance->finalize(); } + + private function fixTimestamp(int $timestamp): int { + if ($this->streamerInstance instanceof ZipStreamer) { + // Zip does not support any timezone information + // while tar is interpreted as Unix time the Zip time is interpreted as local time of the user... + $zone = \OCP\Server::get(IDateTimeZone::class)->getTimeZone($timestamp); + $timestamp += $zone->getOffset(new \DateTimeImmutable('@' . (string)$timestamp)); + } + return $timestamp; + } } From 3e61569ea214948dbcd77ee31fdc4a17bd366c93 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 14 Aug 2025 10:01:33 +0200 Subject: [PATCH 2/2] refactor(Streamer): inject `IDateTimeZone` as constructor arg Signed-off-by: Ferdinand Thiessen --- apps/dav/lib/Connector/Sabre/ServerFactory.php | 2 ++ apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php | 4 +++- apps/dav/lib/Server.php | 2 ++ lib/private/Streamer.php | 9 +++++++-- lib/public/AppFramework/Http/ZipResponse.php | 3 ++- 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php index 8aa9693d6522e..8279cd7c3f381 100644 --- a/apps/dav/lib/Connector/Sabre/ServerFactory.php +++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php @@ -20,6 +20,7 @@ use OCP\Files\IFilenameValidator; use OCP\Files\Mount\IMountManager; use OCP\IConfig; +use OCP\IDateTimeZone; use OCP\IDBConnection; use OCP\IL10N; use OCP\IPreview; @@ -79,6 +80,7 @@ public function createServer(string $baseUri, $objectTree, $this->logger, $this->eventDispatcher, + \OCP\Server::get(IDateTimeZone::class), )); // Some WebDAV clients do require Class 2 WebDAV support (locking), since diff --git a/apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php b/apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php index f198519b45409..de802a3cd50ff 100644 --- a/apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php +++ b/apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php @@ -15,6 +15,7 @@ use OCP\Files\File as NcFile; use OCP\Files\Folder as NcFolder; use OCP\Files\Node as NcNode; +use OCP\IDateTimeZone; use Psr\Log\LoggerInterface; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; @@ -41,6 +42,7 @@ public function __construct( private Tree $tree, private LoggerInterface $logger, private IEventDispatcher $eventDispatcher, + private IDateTimeZone $timezoneFactory, ) { } @@ -163,7 +165,7 @@ public function handleDownload(Request $request, Response $response): ?bool { // Full folder is loaded to rename the archive to the folder name $archiveName = $folder->getName(); } - $streamer = new Streamer($tarRequest, -1, count($content)); + $streamer = new Streamer($tarRequest, -1, count($content), $this->timezoneFactory); $streamer->sendHeaders($archiveName); // For full folder downloads we also add the folder itself to the archive if (empty($files)) { diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index 9bba2f9dcaeab..d8bf23fd0ce11 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -75,6 +75,7 @@ use OCP\IAppConfig; use OCP\ICacheFactory; use OCP\IConfig; +use OCP\IDateTimeZone; use OCP\IPreview; use OCP\IRequest; use OCP\IUserSession; @@ -230,6 +231,7 @@ public function __construct( $this->server->tree, $logger, $eventDispatcher, + \OCP\Server::get(IDateTimeZone::class), )); $this->server->addPlugin(\OCP\Server::get(PaginatePlugin::class)); diff --git a/lib/private/Streamer.php b/lib/private/Streamer.php index ca03295b02678..7ebb66bf03a2b 100644 --- a/lib/private/Streamer.php +++ b/lib/private/Streamer.php @@ -41,7 +41,12 @@ public static function isUserAgentPreferTar(IRequest $request): bool { * @param int $numberOfFiles The number of files (and directories) that will * be included in the streamed file */ - public function __construct(IRequest|bool $preferTar, int|float $size, int $numberOfFiles) { + public function __construct( + IRequest|bool $preferTar, + int|float $size, + int $numberOfFiles, + private IDateTimeZone $timezoneFactory, + ) { if ($preferTar instanceof IRequest) { $preferTar = self::isUserAgentPreferTar($preferTar); } @@ -197,7 +202,7 @@ private function fixTimestamp(int $timestamp): int { if ($this->streamerInstance instanceof ZipStreamer) { // Zip does not support any timezone information // while tar is interpreted as Unix time the Zip time is interpreted as local time of the user... - $zone = \OCP\Server::get(IDateTimeZone::class)->getTimeZone($timestamp); + $zone = $this->timezoneFactory->getTimeZone($timestamp); $timestamp += $zone->getOffset(new \DateTimeImmutable('@' . (string)$timestamp)); } return $timestamp; diff --git a/lib/public/AppFramework/Http/ZipResponse.php b/lib/public/AppFramework/Http/ZipResponse.php index a552eb1294f18..f0c6577f0d4d2 100644 --- a/lib/public/AppFramework/Http/ZipResponse.php +++ b/lib/public/AppFramework/Http/ZipResponse.php @@ -9,6 +9,7 @@ use OC\Streamer; use OCP\AppFramework\Http; +use OCP\IDateTimeZone; use OCP\IRequest; /** @@ -65,7 +66,7 @@ public function callback(IOutput $output) { $size += $resource['size']; } - $zip = new Streamer($this->request, $size, $files); + $zip = new Streamer($this->request, $size, $files, \OCP\Server::get(IDateTimeZone::class)); $zip->sendHeaders($this->name); foreach ($this->resources as $resource) {