Merge pull request #54402 from nextcloud/fix/streamer-mtime-zip

fix(Streamer): use localtime for ZIP files
pull/54377/head
Ferdinand Thiessen 2025-08-18 14:39:37 +07:00 committed by GitHub
commit cd550d57ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 28 additions and 5 deletions

@ -28,6 +28,7 @@ use OCP\Files\IFilenameValidator;
use OCP\Files\IRootFolder; use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountManager; use OCP\Files\Mount\IMountManager;
use OCP\IConfig; use OCP\IConfig;
use OCP\IDateTimeZone;
use OCP\IDBConnection; use OCP\IDBConnection;
use OCP\IGroupManager; use OCP\IGroupManager;
use OCP\IL10N; use OCP\IL10N;
@ -108,6 +109,7 @@ class ServerFactory {
$tree, $tree,
$this->logger, $this->logger,
$this->eventDispatcher, $this->eventDispatcher,
\OCP\Server::get(IDateTimeZone::class),
)); ));
// Some WebDAV clients do require Class 2 WebDAV support (locking), since // Some WebDAV clients do require Class 2 WebDAV support (locking), since

@ -15,6 +15,7 @@ use OCP\Files\Events\BeforeZipCreatedEvent;
use OCP\Files\File as NcFile; use OCP\Files\File as NcFile;
use OCP\Files\Folder as NcFolder; use OCP\Files\Folder as NcFolder;
use OCP\Files\Node as NcNode; use OCP\Files\Node as NcNode;
use OCP\IDateTimeZone;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Sabre\DAV\Server; use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin; use Sabre\DAV\ServerPlugin;
@ -41,6 +42,7 @@ class ZipFolderPlugin extends ServerPlugin {
private Tree $tree, private Tree $tree,
private LoggerInterface $logger, private LoggerInterface $logger,
private IEventDispatcher $eventDispatcher, private IEventDispatcher $eventDispatcher,
private IDateTimeZone $timezoneFactory,
) { ) {
} }
@ -163,7 +165,7 @@ class ZipFolderPlugin extends ServerPlugin {
// Full folder is loaded to rename the archive to the folder name // Full folder is loaded to rename the archive to the folder name
$archiveName = $folder->getName(); $archiveName = $folder->getName();
} }
$streamer = new Streamer($tarRequest, -1, count($content)); $streamer = new Streamer($tarRequest, -1, count($content), $this->timezoneFactory);
$streamer->sendHeaders($archiveName); $streamer->sendHeaders($archiveName);
// For full folder downloads we also add the folder itself to the archive // For full folder downloads we also add the folder itself to the archive
if (empty($files)) { if (empty($files)) {

@ -83,6 +83,7 @@ use OCP\FilesMetadata\IFilesMetadataManager;
use OCP\IAppConfig; use OCP\IAppConfig;
use OCP\ICacheFactory; use OCP\ICacheFactory;
use OCP\IConfig; use OCP\IConfig;
use OCP\IDateTimeZone;
use OCP\IDBConnection; use OCP\IDBConnection;
use OCP\IGroupManager; use OCP\IGroupManager;
use OCP\IPreview; use OCP\IPreview;
@ -249,6 +250,7 @@ class Server {
$this->server->tree, $this->server->tree,
$logger, $logger,
$eventDispatcher, $eventDispatcher,
\OCP\Server::get(IDateTimeZone::class),
)); ));
$this->server->addPlugin(\OCP\Server::get(PaginatePlugin::class)); $this->server->addPlugin(\OCP\Server::get(PaginatePlugin::class));
$this->server->addPlugin(new PropFindPreloadNotifyPlugin()); $this->server->addPlugin(new PropFindPreloadNotifyPlugin());

@ -14,6 +14,7 @@ use OCP\Files\InvalidPathException;
use OCP\Files\IRootFolder; use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException; use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException; use OCP\Files\NotPermittedException;
use OCP\IDateTimeZone;
use OCP\IRequest; use OCP\IRequest;
use ownCloud\TarStreamer\TarStreamer; use ownCloud\TarStreamer\TarStreamer;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -40,7 +41,12 @@ class Streamer {
* @param int $numberOfFiles The number of files (and directories) that will * @param int $numberOfFiles The number of files (and directories) that will
* be included in the streamed file * 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) { if ($preferTar instanceof IRequest) {
$preferTar = self::isUserAgentPreferTar($preferTar); $preferTar = self::isUserAgentPreferTar($preferTar);
} }
@ -156,7 +162,7 @@ class Streamer {
$options = []; $options = [];
if ($time) { if ($time) {
$options = [ $options = [
'timestamp' => $time 'timestamp' => $this->fixTimestamp($time),
]; ];
} }
@ -176,7 +182,7 @@ class Streamer {
public function addEmptyDir(string $dirName, int $timestamp = 0): bool { public function addEmptyDir(string $dirName, int $timestamp = 0): bool {
$options = null; $options = null;
if ($timestamp > 0) { if ($timestamp > 0) {
$options = ['timestamp' => $timestamp]; $options = ['timestamp' => $this->fixTimestamp($timestamp)];
} }
return $this->streamerInstance->addEmptyDir($dirName, $options); return $this->streamerInstance->addEmptyDir($dirName, $options);
@ -191,4 +197,14 @@ class Streamer {
public function finalize() { public function finalize() {
return $this->streamerInstance->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 = $this->timezoneFactory->getTimeZone($timestamp);
$timestamp += $zone->getOffset(new \DateTimeImmutable('@' . (string)$timestamp));
}
return $timestamp;
}
} }

@ -9,6 +9,7 @@ namespace OCP\AppFramework\Http;
use OC\Streamer; use OC\Streamer;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\IDateTimeZone;
use OCP\IRequest; use OCP\IRequest;
/** /**
@ -65,7 +66,7 @@ class ZipResponse extends Response implements ICallbackResponse {
$size += $resource['size']; $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); $zip->sendHeaders($this->name);
foreach ($this->resources as $resource) { foreach ($this->resources as $resource) {